TCP編程詳解


TCP把鏈接做爲最基本的對象,每一條TCP鏈接都有兩個端點,這種端點咱們稱爲套接字(socket)。端口號拼接到IP地址後面就構成了套接字,例如192.0.0.32:80。IP協議雖然能把數據報文送到目的主機,可是並無交付給主機的具體應用進程,而端到端的通訊纔是應用進程之間的通訊。html

數據包格式

TCP報文是TCP層傳輸的數據單元,也叫報文段。TCP報文的格式以下圖:
DGb0Xj.png
TCP報文中的標誌位:編程

  1. URG:緊急指針標誌,爲1時表示緊機指針有效,爲0時則忽略緊急指針
  2. ACK: 確認序號標誌。爲1時表示確認號有效,爲0時表示報文中不含確認信息,忽略確認號字段。
  3. PSH: push標誌,爲1表示是帶有push標誌的數據,指示接收方在接收到該報文段之後,應儘快將這個報文段交給應用程序,而不是在緩衝區排隊
  4. RST: 重置鏈接標誌,用於重置因爲主機崩潰或其餘緣由而出現錯誤的鏈接。或者用於拒絕非法的報文段和拒絕鏈接請求。
  5. SYN: 同步序號,用於創建鏈接過程,在鏈接請求中,SYN=1和ACK=0表示該數據段沒有使用捎帶的確認域,而鏈接應答捎帶一個確認,即SYN=1和ACK=1
  6. FIN: finish標誌,用於釋放鏈接,爲1時表示發送方已經沒有數據發送了,即關閉本方數據流

創建鏈接(三次握手)

TCP通訊時的創建鏈接須要三次握手,意思是創建鏈接的時候,客戶端與服務器之間須要三次數據包的交流。
DJSGHe.png服務器

  1. 客戶端發送給服務器一個請求鏈接數據包,即發送了一個指向服務器目標端口的一個SYN位爲1的TCP報文
  2. 服務器接收到客戶端的鏈接請求以後,會迴應一個SYN位爲1的TCP報文,表示贊成鏈接。而且會把ACK位也置1表示確認收到上次消息
  3. 客戶端收到服務器的贊成鏈接的數據包以後,還要回復一個ACK位1的TCP報文,表示確認收到

數據傳輸

TCP是以段爲單位發送數據的,在創建TCP鏈接的同時,每一個數據包的長度也被肯定下來,通常稱其爲最大消息長度(MSS: Maximum Segment Size)。TCP在傳輸大量數據時,是以MSS的大小將數據進行分割發送的,進行重發時也是以MSS爲單位。兩端的主機在發出創建鏈接的請求時,會在TCP首部中寫入MSS選項,告訴對方本身的接口可以適應的MSS的大小。爲附加MSS選項,TCP的首部將再也不是20字節,而是4字節的整數倍。會在二者之間選擇一個較小的值投入使用。
DJSXgx.pngsocket

斷開鏈接(四次揮手)

四次揮手,意思就是釋放鏈接的時候客戶端與服務器之間須要四次數據包的交流。
DJ9FwF.pngtcp

  1. 客戶端發送給服務器一個請求釋放鏈接的數據包,即發送了一個指向服務器目標端口的一個FIN位爲1的TCP報文,表示客戶端沒有數據要發送了,可是仍然能夠接收數據。而且ACK位也爲1,表示對上次傳輸數據結果的確認。而且以後處於等待狀態,等待服務器的兩次迴應
  2. 服務器接收到客戶端的釋放鏈接請求以後,會先回應一個ACK位爲1的報文,表示確認收到。可是這時服務器可能還有數據沒有發送完成,繼續發送數據
  3. 服務器發送完數據以後,發送一個FIN爲1的TCP報文,表示我也沒有要發送的數據了,你能夠釋放鏈接了,固然ACK位仍爲1
  4. 客戶端接受到服務器的贊成釋放鏈接的數據包以後,回覆一個ACK爲1的TCP報文,表示數據收到

基礎

socket編程通常採用客戶端-服務器模式(即由客戶進程向服務器進程發出請求,服務器進程執行請求的任務並將執行結果返回給客戶進程的模式)ide

客戶端流程

TCP客戶端socket編程流程函數

  1. 建立socket:Socket()
  2. 創建鏈接:Connect()
  3. 通訊:Send()Recv()
  4. 關閉socket: CloseSocket()

編碼

  1. hostent結構體
hostent結構體

The hostent structure is defined in <netdb.h> as follows:

struct hostent {
   char  *h_name;            /* official name of host */
   char **h_aliases;         /* alias list */
   int    h_addrtype;        /* host address type */
   int    h_length;          /* length of address */
   char **h_addr_list;       /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */

The members of the hostent structure are:

h_name 
  The official name of the host.

h_aliases
  An array of alternative names for the host, terminated by a NULL pointer.

h_addrtype
  The type of address; always AF_INET or AF_INET6 at present.

h_length
  The length of the address in bytes.

h_addr_list
  An array of pointers to network addresses for the host (in network byte order), terminated by a NULL pointer.

h_addr The first address in h_addr_list for backward compatibility.
  1. gethostbyname & gethostbyaddr 過期了,應用應該使用getaddrinfo和getnameinfo.TCP客戶端
static const char send_data[] = "This is TCP Client from RT-Thread."; /* 發送用到的數據 */
void tcpclient(int argc, char **argv)
{
    int ret;
    char *recv_data;
    struct hostent *host;
    int sock, bytes_received;
    struct sockaddr_in server_addr;
    const char *url;
    int port;

    if (argc < 3)
    {
        rt_kprintf("Usage: tcpclient URL PORT\n");
        rt_kprintf("Like: tcpclient 192.168.12.44 5000\n");
        return ;
    }

    url = argv[1];
    port = strtoul(argv[2], 0, 10);

    /* 經過函數入口參數url得到host地址(若是是域名,會作域名解析) */
    host = gethostbyname(url);

    /* 分配用於存放接收數據的緩衝 */
    recv_data = rt_malloc(BUFSZ);
    if (recv_data == RT_NULL)
    {
        rt_kprintf("No memory\n");
        return;
    }

    /* 建立一個socket,類型是SOCKET_STREAM,TCP類型 */
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        /* 建立socket失敗 */
        rt_kprintf("Socket error\n");

        /* 釋放接收緩衝 */
        rt_free(recv_data);
        return;
    }

    /* 初始化預鏈接的服務端地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr = *((struct in_addr *)host->h_addr);
    rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));

    /* 鏈接到服務端 */
    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
    {
        /* 鏈接失敗 */
        rt_kprintf("Connect fail!\n");
        closesocket(sock);

        /*釋放接收緩衝 */
        rt_free(recv_data);
        return;
    }
    else
    {
        /* 鏈接成功 */
        rt_kprintf("Connect successful\n");
    }

    while (1)
    {
        /* 從sock鏈接中接收最大BUFSZ - 1字節數據 */
        bytes_received = recv(sock, recv_data, BUFSZ - 1, 0);
        if (bytes_received < 0)
        {
            /* 接收失敗,關閉這個鏈接 */
            closesocket(sock);
            rt_kprintf("\nreceived error,close the socket.\r\n");

            /* 釋放接收緩衝 */
            rt_free(recv_data);
            break;
        }
        else if (bytes_received == 0)
        {
            /* 默認 recv 爲阻塞模式,此時收到0認爲鏈接出錯,關閉這個鏈接 */
            closesocket(sock);
            rt_kprintf("\nreceived error,close the socket.\r\n");

            /* 釋放接收緩衝 */
            rt_free(recv_data);
            break;
        }

        /* 有接收到數據,把末端清零 */
        recv_data[bytes_received] = '\0';

        if (strncmp(recv_data, "q", 1) == 0 || strncmp(recv_data, "Q", 1) == 0)
        {
            /* 若是是首字母是q或Q,關閉這個鏈接 */
            closesocket(sock);
            rt_kprintf("\n got a 'q' or 'Q',close the socket.\r\n");

            /* 釋放接收緩衝 */
            rt_free(recv_data);
            break;
        }
        else
        {
            /* 在控制終端顯示收到的數據 */
            rt_kprintf("\nReceived data = %s ", recv_data);
        }

        /* 發送數據到sock鏈接 */
        ret = send(sock, send_data, strlen(send_data), 0);
        if (ret < 0)
        {
            /* 接收失敗,關閉這個鏈接 */
            closesocket(sock);
            rt_kprintf("\nsend error,close the socket.\r\n");

            rt_free(recv_data);
            break;
        }
        else if (ret == 0)
        {
            /* 打印send函數返回值爲0的警告信息 */
            rt_kprintf("\n Send warning,send function return 0.\r\n");
        }
    }
    return;
}

TCP服務端流程

  1. 建立socket:socket()
  2. 將建立的socket綁定到一個IP地址和端口號上:bind()
  3. 設置socket爲監聽模式:listen()
  4. 接受請求並返回socket:accept()。accept會阻塞住等待接收消息
  5. 與客戶端進行通訊recv() & send()
  6. 關閉socket:close()

TCP服務端編碼

static void tcpserv(int argc, char **argv)
{
    char *recv_data; /* 用於接收的指針,後面會作一次動態分配以請求可用內存 */
    socklen_t sin_size;
    int sock, connected, bytes_received;
    struct sockaddr_in server_addr, client_addr;
    rt_bool_t stop = RT_FALSE; /* 中止標誌 */
    int ret;

    recv_data = rt_malloc(BUFSZ + 1); /* 分配接收用的數據緩衝 */
    if (recv_data == RT_NULL)
    {
        rt_kprintf("No memory\n");
        return;
    }

    /* 一個socket在使用前,須要預先建立出來,指定SOCK_STREAM爲TCP的socket */
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        /* 建立失敗的錯誤處理 */
        rt_kprintf("Socket error\n");

        /* 釋放已分配的接收緩衝 */
        rt_free(recv_data);
        return;
    }

    /* 初始化服務端地址 */
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(5000); /* 服務端工做的端口 */
    server_addr.sin_addr.s_addr = INADDR_ANY;
    rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));

    /* 綁定socket到服務端地址 */
    if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
    {
        /* 綁定失敗 */
        rt_kprintf("Unable to bind\n");

        /* 釋放已分配的接收緩衝 */
        rt_free(recv_data);
        return;
    }

    /* 在socket上進行監聽 */
    if (listen(sock, 5) == -1)
    {
        rt_kprintf("Listen error\n");

        /* release recv buffer */
        rt_free(recv_data);
        return;
    }

    rt_kprintf("\nTCPServer Waiting for client on port 5000...\n");
    while (stop != RT_TRUE)
    {
        sin_size = sizeof(struct sockaddr_in);

        /* 接受一個客戶端鏈接socket的請求,這個函數調用是阻塞式的 */
        connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);
        /* 返回的是鏈接成功的socket */
        if (connected < 0)
        {
            rt_kprintf("accept connection failed! errno = %d\n", errno);
            continue;
        }

        /* 接受返回的client_addr指向了客戶端的地址信息 */
        rt_kprintf("I got a connection from (%s , %d)\n",
                   inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        /* 客戶端鏈接的處理 */
        while (1)
        {
            /* 發送數據到connected socket */
            ret = send(connected, send_data, strlen(send_data), 0);
            if (ret < 0)
            {
                /* 發送失敗,關閉這個鏈接 */
                closesocket(connected);
                rt_kprintf("\nsend error,close the socket.\r\n");
                break;
            }
            else if (ret == 0)
            {
                /* 打印send函數返回值爲0的警告信息 */
                rt_kprintf("\n Send warning,send function return 0.\r\n");
            }

            /* 從connected socket中接收數據,接收buffer是1024大小,但並不必定可以收到1024大小的數據 */
            bytes_received = recv(connected, recv_data, BUFSZ, 0);
            if (bytes_received < 0)
            {
                /* 接收失敗,關閉這個connected socket */
                closesocket(connected);
                break;
            }
            else if (bytes_received == 0)
            {
                /* 打印recv函數返回值爲0的警告信息 */
                rt_kprintf("\nReceived warning,recv function return 0.\r\n");
                closesocket(connected);
                break;
            }

            /* 有接收到數據,把末端清零 */
            recv_data[bytes_received] = '\0';
            if (strcmp(recv_data, "q") == 0 || strcmp(recv_data, "Q") == 0)
            {
                /* 若是是首字母是q或Q,關閉這個鏈接 */
                closesocket(connected);
                break;
            }
            else if (strcmp(recv_data, "exit") == 0)
            {
                /* 若是接收的是exit,則關閉整個服務端 */
                closesocket(connected);
                stop = RT_TRUE;
                break;
            }
            else
            {
                /* 在控制終端顯示收到的數據 */
                rt_kprintf("RECEIVED DATA = %s \n", recv_data);
            }
        }
    }

    /* 退出服務 */
    closesocket(sock);

    /* 釋放接收緩衝 */
    rt_free(recv_data);

    return ;
}

參考文獻

  1. RT-Thread視頻中心內核入門
  2. RT-Thread文檔中心

本文做者: CrazyCatJackui

本文連接: https://www.cnblogs.com/CrazyCatJack/p/14408903.html編碼

版權聲明:本博客全部文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!url

關注博主:若是您以爲該文章對您有幫助,能夠點擊文章右下角推薦一下,您的支持將成爲我最大的動力!

相關文章
相關標籤/搜索