找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 2345|回复: 0
打印 上一主题 下一主题
收起左侧

TcpSever 和 三次握手

[复制链接]
跳转到指定楼层
楼主
ID:99624 发表于 2015-12-22 23:57 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
总是认为on MCU上实现TCP sever之流的应用是扯淡,谁会去连接呢。而且一度认为和客户端并没有区别,没什么用。直到最近才发现其实并没不是那样,最不济做IAP嘛。我想对我来说这都是空想,临渊慕鱼不如退而结网,我只要实现一个TCP sever就算是走了一小步了吧。

1、Socket 的TCP sever 代码(C++)
SOCKET server = socket(AF_INET,SOCK_STREAM,0);
if(server == INVALID_SOCKET)
{
   DWORD err1 = WSAGetLastError();
  return;
}
    SOCKADDR_IN sockAddr;
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(1234);
int flag1 = ::bind(server,(const sockaddr*)&sockAddr,sizeof(sockAddr));
if(flag1 == SOCKET_ERROR)
{
  DWORD err2 = WSAGetLastError();
  return;
}
else
int flag2 = ::listen(server,5);
if(flag2 == SOCKET_ERROR)
{
  DWORD err3 = WSAGetLastError();
  return;
  }
int len = sizeof(sockaddr);
SOCKET s = ::accept(server,(sockaddr*)&sockAddr,&len);
if(s == INVALID_SOCKET)
{
  DWORD err4 = WSAGetLastError();
  closesocket(s);
  WSACleanup();
  }

可见大体上分为这么几步:Create Socket、Bind Socket、Listen Socket 、Accept Socket。

2、分解(RAW API)
2.1 Create Socket
从应用层看来是建立了一个socket实际上socket仅仅是个索引而已。真正起作用的是TCPIP的协议控制块。所谓的PCB。所以分析的时候还是RAW API 创建的时候PCB会动态的从内存中分配出来,这个内存是在内存管理的大块内存中切割出来一块用于TCP PCB的一小块内存块,然后再在这块内存块中再次切割一块TCP PCB 所需的大小的内存块。内存已经对齐并且是PCB的正整数倍。广度由这个宏来定义。MEMP_NUM_TCP_PCB 。实际上是个二维数组,TCP PCB所在的地方被包含内。于是PCB就被创建起来,他的唯一ID是他的首地址。用一个指针来存储。这样就跑不了。
建立PCB后开始对他初始化这是老生常谈了,TTL\WND\SENDBUF\TRM被初始化。之后完成了建立PCB这一重要任务。
2.2 Bind Socket
绑定端口到这个PCB中,这个简单系统自动检查一下端口是不是有效,如果有效就直接保存进上面创建的PCB中,如果NULL则new_port()一个出来给他。new_port大有来头,也就是著名的TCP_LOCAL_PORT_RANGE_END - TCP_LOCAL_PORT_RANGE_START。哈哈在这里了吧。
最后一步很重要,将这个PCB注册到绑定表tcp_bound_pcbs。
注册绑定表实际上仅仅是一个指针变量。他是如何做成表的呢?他并不是数组,怎么做个表,原来他是链表哦。哈哈明白了?
     (npcb)->next = *pcbs;                       
    *(pcbs) = (npcb);   
每一个PCB中有一个next ,第一步他先取得 tcp_bound_pcbs的地址并交给要注册的PCB的(npcb)->next 中去,这样,PCB就知道tcp_bound_pcbs的位置了,第二步,把当前PCB的值给tcp_bound_pcbs。这样当前的 tcp_bound_pcbs总是指向最后注册的一个PCB上,如果在来第三个第四个一次进行,如果只有两个那么当前PCB的next刚好==NULL,这样这个链表就OK了,只要netx不是NULL就表示有PCB在绑定的队列中。
这很重要,因为几乎所有的表全部是这样的存储方式。同时也是个坑。是个大坑。搞不好就被咬住。试想if pcbs==pcb->next。会有什么后果呢?网友们跑程序死机的问题多多的线索之一啦!!! 现实中我也遇到了,否则也不可能去看他的结构的。我的问题是程序运行一段时间后阻塞在下面
++tcp_timer_ctr
pcb = tcp_active_pcbs;
while(pcb != NULL) {<<<<<<<<<<<<<<-----------------------MARK1
if (pcb->last_timer != tcp_timer_ctr) {<<<<<<<<<<<<<<-----------------------MARK2
struct tcp_pcb *next;
pcb->last_timer = tcp_timer_ctr;
// do something ....   send delayed ACKs
next = pcb->next;<<<<<<<<<<<<<<-----------------------MARK3
//do something  refused
pcb = next;<<<<<<<<<<<<<<-----------------------MARK4
                                }
这是TCP的FAST TIME服务里面对LAST time的更新。表面上停留在一个和时间有关的项上,实际上和时间半毛钱关系都没有,逐句看下去就发现第一tcp_timer_ctr在开始进入的永远都比pcb->last_timer高一个数,因为++tcp_timer_ctr 的存在,所以问题肯定不在这一上一定在下面,下面的是next;这个再配合while(pcb != NULL) 加上 pcb->last_timer = tcp_timer_ctr;他的退出的条件是是==NULL,所以咯.....跟踪变量发现pcb->next正好等于当前tcp_active_pcbs。。为了方便追踪加了一行代码
        if(pcb==pcb->next)
                {
                  // DEBUG
             break;
                }
每次出异常都会进入。验证了之前的推断了。这证明链表被破坏了,他的next和当前地址是一个地址。这是违背链表判断条件的。后来查出来是第一个PCB没有移除tcp_active_pcbs值却释放了PCB的内存,这样来了新连接又会分配了一个和他相同的地址,然后又被加入表中。在注册中并没有做相关的判断保护。最后确定是应用程序的问题。导致的。
2.3 Listen Socket
如果对PCB进行listen操作那么首先是再分一个PCB,把上一个PCB里面的内容拷贝到这个刚创建的PCB中,之前不是绑定了么,然后解除 tcp_bound_pcbs,然后重新注册到tcp_listen_pcbs中去。然后释放掉最原始的那个,返回当前创建的,这样PCB摇身一变。对用户来说是不知道的。实际上PCB的地址已经改变,原来的PCB已经释放掉了,所以又返回了一个新的PCB地址。这个PCB被加入到listen表。另外PCB的状态变成了LINSTEN 这很重要。
  2.4 Accept Socket
accept实际上仅仅是个回调函数而已。如果忘记了,没关系。系统早已经考虑到忘记,早有一个空的被注册进去,如果这时候你accept一个仅仅是 do  pcb->accept = accept  而已。
想要知道这个accept是干什么用的,就需要看TCP的状态机,看看这个在哪里被调用呢?这就引入另一个问题。连接如何进来?如何处理、accept 是干什么的?这就是下一点--连接
3、连接(TCP的3次握手)
那么要知道TCP数据怎么来就需要看IP层,找到IP层传递函数,然后在TCPIN里查看数据如何被分配的。
话说一个包含这个SYN的以太网帧进入协议栈后被一系列的检测过滤、直到他走到遍历PCB是否在LINSTEN表中的时候(无疑PCB状态是LINSTEN当然在表中) 条件满足
for(lpcb = tcp_listen_pcbs.listen_pcbs; lpcb != NULL; lpcb = lpcb->next)
遍历链表啦。他一定在哪里等着,条件又满足于是乎就会调用 tcp_listen_input(lpcb);这个函数。
又开始检查SYN标志,还是满足。那么这就是第一次握手,
下一步就是动态分配一个PCB,并做必要的填空题。
     npcb = tcp_alloc(pcb->prio);
    ip_addr_copy(npcb->local_ip, current_iphdr_dest);
    npcb->local_port = pcb->local_port;
    ip_addr_copy(npcb->remote_ip, current_iphdr_src);
    npcb->remote_port = tcphdr->src;
    npcb->state = SYN_RCVD;
    npcb->rcv_nxt = seqno + 1;
    npcb->rcv_ann_right_edge = npcb->rcv_nxt;
    npcb->snd_wnd = tcphdr->wnd;
    npcb->snd_wnd_max = tcphdr->wnd;
    npcb->ssthresh = npcb->snd_wnd;
注意现在状态是SYN_RCVD,同时会发会SYNACK ,完成第二次握手 ,此时PCB被加入到活跃链表中。在这器件会等待最后一个SYN ACK 的ACK如果TCP_SYN_RCVD_TIMEOUT时间不来。怎么办?果断移除,连接失败。正常是客户端回应了ACK 这样就会沿着tcp_process进入,当前是SYN_RCVD收到ACK,也就是第三次握手了。
case SYN_RCVD:
    if (flags & TCP_ACK) {
      /* expected ACK number? */
      if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt)) {
        u16_t old_cwnd;
        pcb->state = ESTABLISHED;
      /* Call the accept function. */
        TCP_EVENT_ACCEPT(pcb, ERR_OK, err);
显然状态变成了ESTABLISHED;到这里TCP 的三次握手全部建立。TCP进入随时可以收发数据的状态。也就是正常状态了。
OK 最后一个问题就是accept,上面的代码可见,变换完毕状态之后立马TCP_EVENT_ACCEPT(pcb, ERR_OK, err); 这就是accept函数了。这个函数最后执行自定义的注册的   pcb->accept !!!
也就是每有一个客户端连接过来并且3次握手成功后就会调用 pcb->accept 。于是乎他的作用就是告诉应用程序,TCP连接建立了,你准备吧。这就是在accpet函数中要执行 tcp_recv(pcb, my_recv)的原因,这样就可以收到来自客户端的数据了


20151106
laowangtou
bt



分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享淘帖 顶 踩
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表