找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 2896|回復(fù): 0
打印 上一主題 下一主題
收起左側(cè)

TcpSever 和 三次握手

[復(fù)制鏈接]
跳轉(zhuǎn)到指定樓層
樓主
ID:99624 發(fā)表于 2015-12-22 23:57 | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
總是認(rèn)為on MCU上實現(xiàn)TCP sever之流的應(yīng)用是扯淡,誰會去連接呢。而且一度認(rèn)為和客戶端并沒有區(qū)別,沒什么用。直到最近才發(fā)現(xiàn)其實并沒不是那樣,最不濟做IAP嘛。我想對我來說這都是空想,臨淵慕魚不如退而結(jié)網(wǎng),我只要實現(xiàn)一個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
從應(yīng)用層看來是建立了一個socket實際上socket僅僅是個索引而已。真正起作用的是TCPIP的協(xié)議控制塊。所謂的PCB。所以分析的時候還是RAW API 創(chuàng)建的時候PCB會動態(tài)的從內(nèi)存中分配出來,這個內(nèi)存是在內(nèi)存管理的大塊內(nèi)存中切割出來一塊用于TCP PCB的一小塊內(nèi)存塊,然后再在這塊內(nèi)存塊中再次切割一塊TCP PCB 所需的大小的內(nèi)存塊。內(nèi)存已經(jīng)對齊并且是PCB的正整數(shù)倍。廣度由這個宏來定義。MEMP_NUM_TCP_PCB 。實際上是個二維數(shù)組,TCP PCB所在的地方被包含內(nèi)。于是PCB就被創(chuàng)建起來,他的唯一ID是他的首地址。用一個指針來存儲。這樣就跑不了。
建立PCB后開始對他初始化這是老生常談了,TTL\WND\SENDBUF\TRM被初始化。之后完成了建立PCB這一重要任務(wù)。
2.2 Bind Socket
綁定端口到這個PCB中,這個簡單系統(tǒng)自動檢查一下端口是不是有效,如果有效就直接保存進上面創(chuàng)建的PCB中,如果NULL則new_port()一個出來給他。new_port大有來頭,也就是著名的TCP_LOCAL_PORT_RANGE_END - TCP_LOCAL_PORT_RANGE_START。哈哈在這里了吧。
最后一步很重要,將這個PCB注冊到綁定表tcp_bound_pcbs。
注冊綁定表實際上僅僅是一個指針變量。他是如何做成表的呢?他并不是數(shù)組,怎么做個表,原來他是鏈表哦。哈哈明白了?
     (npcb)->next = *pcbs;                       
    *(pcbs) = (npcb);   
每一個PCB中有一個next ,第一步他先取得 tcp_bound_pcbs的地址并交給要注冊的PCB的(npcb)->next 中去,這樣,PCB就知道tcp_bound_pcbs的位置了,第二步,把當(dāng)前PCB的值給tcp_bound_pcbs。這樣當(dāng)前的 tcp_bound_pcbs總是指向最后注冊的一個PCB上,如果在來第三個第四個一次進行,如果只有兩個那么當(dāng)前PCB的next剛好==NULL,這樣這個鏈表就OK了,只要netx不是NULL就表示有PCB在綁定的隊列中。
這很重要,因為幾乎所有的表全部是這樣的存儲方式。同時也是個坑。是個大坑。搞不好就被咬住。試想if pcbs==pcb->next。會有什么后果呢?網(wǎng)友們跑程序死機的問題多多的線索之一啦。! 現(xiàn)實中我也遇到了,否則也不可能去看他的結(jié)構(gòu)的。我的問題是程序運行一段時間后阻塞在下面
++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服務(wù)里面對LAST time的更新。表面上停留在一個和時間有關(guān)的項上,實際上和時間半毛錢關(guān)系都沒有,逐句看下去就發(fā)現(xiàn)第一tcp_timer_ctr在開始進入的永遠(yuǎn)都比pcb->last_timer高一個數(shù),因為++tcp_timer_ctr 的存在,所以問題肯定不在這一上一定在下面,下面的是next;這個再配合while(pcb != NULL) 加上 pcb->last_timer = tcp_timer_ctr;他的退出的條件是是==NULL,所以咯.....跟蹤變量發(fā)現(xiàn)pcb->next正好等于當(dāng)前tcp_active_pcbs。。為了方便追蹤加了一行代碼
        if(pcb==pcb->next)
                {
                  // DEBUG
             break;
                }
每次出異常都會進入。驗證了之前的推斷了。這證明鏈表被破壞了,他的next和當(dāng)前地址是一個地址。這是違背鏈表判斷條件的。后來查出來是第一個PCB沒有移除tcp_active_pcbs值卻釋放了PCB的內(nèi)存,這樣來了新連接又會分配了一個和他相同的地址,然后又被加入表中。在注冊中并沒有做相關(guān)的判斷保護。最后確定是應(yīng)用程序的問題。導(dǎo)致的。
2.3 Listen Socket
如果對PCB進行l(wèi)isten操作那么首先是再分一個PCB,把上一個PCB里面的內(nèi)容拷貝到這個剛創(chuàng)建的PCB中,之前不是綁定了么,然后解除 tcp_bound_pcbs,然后重新注冊到tcp_listen_pcbs中去。然后釋放掉最原始的那個,返回當(dāng)前創(chuàng)建的,這樣PCB搖身一變。對用戶來說是不知道的。實際上PCB的地址已經(jīng)改變,原來的PCB已經(jīng)釋放掉了,所以又返回了一個新的PCB地址。這個PCB被加入到listen表。另外PCB的狀態(tài)變成了LINSTEN 這很重要。
  2.4 Accept Socket
accept實際上僅僅是個回調(diào)函數(shù)而已。如果忘記了,沒關(guān)系。系統(tǒng)早已經(jīng)考慮到忘記,早有一個空的被注冊進去,如果這時候你accept一個僅僅是 do  pcb->accept = accept  而已。
想要知道這個accept是干什么用的,就需要看TCP的狀態(tài)機,看看這個在哪里被調(diào)用呢?這就引入另一個問題。連接如何進來?如何處理、accept 是干什么的?這就是下一點--連接
3、連接(TCP的3次握手)
那么要知道TCP數(shù)據(jù)怎么來就需要看IP層,找到IP層傳遞函數(shù),然后在TCPIN里查看數(shù)據(jù)如何被分配的。
話說一個包含這個SYN的以太網(wǎng)幀進入?yún)f(xié)議棧后被一系列的檢測過濾、直到他走到遍歷PCB是否在LINSTEN表中的時候(無疑PCB狀態(tài)是LINSTEN當(dāng)然在表中) 條件滿足
for(lpcb = tcp_listen_pcbs.listen_pcbs; lpcb != NULL; lpcb = lpcb->next)
遍歷鏈表啦。他一定在哪里等著,條件又滿足于是乎就會調(diào)用 tcp_listen_input(lpcb);這個函數(shù)。
又開始檢查SYN標(biāo)志,還是滿足。那么這就是第一次握手,
下一步就是動態(tài)分配一個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;
注意現(xiàn)在狀態(tài)是SYN_RCVD,同時會發(fā)會SYNACK ,完成第二次握手 ,此時PCB被加入到活躍鏈表中。在這器件會等待最后一個SYN ACK 的ACK如果TCP_SYN_RCVD_TIMEOUT時間不來。怎么辦?果斷移除,連接失敗。正常是客戶端回應(yīng)了ACK 這樣就會沿著tcp_process進入,當(dāng)前是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);
顯然狀態(tài)變成了ESTABLISHED;到這里TCP 的三次握手全部建立。TCP進入隨時可以收發(fā)數(shù)據(jù)的狀態(tài)。也就是正常狀態(tài)了。
OK 最后一個問題就是accept,上面的代碼可見,變換完畢狀態(tài)之后立馬TCP_EVENT_ACCEPT(pcb, ERR_OK, err); 這就是accept函數(shù)了。這個函數(shù)最后執(zhí)行自定義的注冊的   pcb->accept 。!
也就是每有一個客戶端連接過來并且3次握手成功后就會調(diào)用 pcb->accept 。于是乎他的作用就是告訴應(yīng)用程序,TCP連接建立了,你準(zhǔn)備吧。這就是在accpet函數(shù)中要執(zhí)行 tcp_recv(pcb, my_recv)的原因,這樣就可以收到來自客戶端的數(shù)據(jù)了


20151106
laowangtou
bt



分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享淘帖 頂 踩
回復(fù)

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規(guī)則

手機版|小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機教程網(wǎng)

快速回復(fù) 返回頂部 返回列表