Linux - socket

什麼是socket

在學習套接口以前,先要回顧一下Tcp/Ip四層模型:編程

而在說明什麼是Socket以前,須要理解下面這些圖:緩存

而實際上:bash

這跟管道是不一樣的,管道只能用於本機的進程間通訊。另外socket能用於異構系統間進行通訊:服務器

IPv4套接口地址結構

通常不用網絡

爲何要有地址家族呢?由於Socket不只僅只能用於Tcp/Ip協議,還能用於其它協議,如:Unix域協議,因此必定得指名是哪一個家族,若是是IPv4協議,則須要指定爲AF_INET,若是是AF_INET6,則就是IPv6協議,這個用得不多~併發

16位的無符號整數,也就是兩個字節,它能表示的最大的整數爲:65535dom

對於IPv4協議,地址是32位的,也就是四個字節,因此該結構體爲無符號的32位整數:socket

實際上,也能夠經過man幫助來看到其結構:man 7 iptcp

【注意】:日常編程時,只會用到sa_family_t、in_port_t、struct in_addr這三個字段。模塊化

通用地址結構

該字段總共有14個字節,實際上跟sockaddr_in最後面三個字段的總和是同樣大小的:

因此說,通用的地址結構能夠兼容IPv4的結構

爲何要有通用地址結構呢? 緣由在於Socket不只僅只能用於Tcp/Ip編程,它還可以用於Unix域協議編程,不一樣的協議地址結構形式可能有不同的地方,因此說,這裏存在一個統一形式的地址結構,能夠用於全部的協議編程。

【提示】:實際編程中,一般是填充sockaddr_in地址結構,最終再強制轉換成通用的sockaddr地址結構。

網絡字節序

實際上,剛纔在查看man幫助時,就出現過這個概念,如:

因此下面來認識一下它:

關於上面的概念,可能有些抽象,下面用圖來講明一下:

爲何要引入字節序這樣一個概念呢?

這是由於Socket能夠用於異構系統之間的通信,不一樣的硬件平臺,對於一個整數,存放形式是不同的,有的機器是採用的大端字節序,有的則採用的小端,若是傳給對等方可能解析出來的數字就會不同了,這時就必須統一字節序,這個字節序就叫作「網絡字節序」,因此能夠看下面介紹。

這裏指的就是本機中的實際字節序,下面能夠編寫一個小小的程序來驗證一下咱們的機器是什麼字節序,以下:

編譯運行:

字節序轉換函數

下面來用代碼來講明一下:

編譯運行:

地址轉換函數

爲何要有地址轉換函數呢?由於咱們日常人爲認識的地址並非32的數字,咱們比較習慣的地址相似於這樣:"192.168.0.100",而咱們編程的時候,更多的是用的32的數字,因此須要引入地址轉換函數,以下:

這個函數的功能跟下面這個函數的功能同樣,都是將用戶識別的地址轉換成網絡字節序,將存放在inp這個結構體中,第二個參數是一個輸出參數。

將用戶識別的相似於"192.168.0.100"這樣的地址轉換成32位的整數,下面用代碼來看一下效果:

編譯運行:

將32位的網絡字節序轉換成咱們能識別的ip形式的地址:

編譯運行:

套接字類型

對於TCP/IP協議而言,就是tcp協議,若是是其它協議而言那就不必定了。

它提供了一種能力,讓咱們跨越傳輸層,直接對ip層進行數據封裝的套接字,經過原始套接字,咱們能夠將應用層的數據直接封裝成ip層可以認識的協議格式,關於原始套接字的編程以後再來學。

TCP客戶/服務器模型

回射客戶/服務器

這個例子的效果就是:客戶端從命令行獲取一行命令,而後發送給服務器端,當服務端接收到這行命令以後,不作任何操做,將其又回送給客戶端,而後客戶端進行回顯,下面則開始一步步來實現這樣的效果,來初步感覺下Socket編程:

首先編寫服務端:echosrv.c

第一步:建立套接字:

關於第一個參數domain,man幫助中也有說明:

可是,AF_INET等價於PF_INET,這裏推薦用後者,由於恰好表明protocol family含義,下面代碼以下:

第二步:綁定一個地址到套接字上:

首先準備一下第二個參數,也就是要綁定的地址:

其中綁定地址還有其它兩種方式:

另外,其實"servaddr.sin_addr.s_addr = htonl(INADDR_ANY);"這種寫法是能夠省略掉的,由於它是全0,但這裏爲了顯示說明因此保留。

下面開始進行綁定:

第三步:則開始進行監聽:

具體代碼以下:

其中SOMAXCONN能夠從man幫助中查看到:

它表明了socket的併發最大鏈接個數。 另外還得注意,套接字有被動套接字和主動套接字之分,當調用listen以後,該socket就變更被動套接字了,須要由主動套接字來發起鏈接,主動套接字是用connect函數來發起鏈接的。

第四步:從已完成鏈接隊列中返回第一個鏈接:

接下來,則進行數據的接收,並將數據回顯給客戶端: accept函數會返回一個新的套接字,注意:此時的套接字再也不是被動套接字,而變爲了主動:

能夠經過accept的man手冊來得知:

下面,則開始從該套接字中讀取客戶端發過來的數據:

至此,服務端的代碼都已經編寫完了,下面則先編譯一下:

查看man幫助:

因而在代碼中加入頭:

再次編譯:

仍是出錯,那IPPPOTO_TCP是在哪定義的呢? 能夠經過如下命令進行查找:

因而乎,加上該頭文件後再編譯:

用一樣的辦法來進行查找:

因而加入它:

再次編譯:

仍是報錯,對於這裏面對應的頭文件這裏就不具體一個個查找了,否則有點充數的嫌疑,將全部頭文件加上再次編譯:

接下來,開始編寫客戶端的代碼:echocli.c 首先建立一個socket:

第二步開始與服務器進行鏈接:

【說明】:用connect發起鏈接的套接字是主動套接字。 鏈接成功以後,就能夠向服務器發送數據了:

另外,服務端在使用資源以後,最後也得關閉掉,因此修改服務端程序以下:

這時,客戶端程序也已經編寫完成,下面編譯運行看一下效果:

也就是第一次客戶端輸入很長的字符串,而第二次輸入很短的字符串時,這時就會輸出有問題,照理應該是客戶端輸入什麼,服務端就會回顯給客戶端,也就是打印兩條如出一轍的語句,產生這樣的問題緣由是什麼呢? 下面來用圖來分析一下:

因此,解決該問題的思路就是每次循環時將值初始化一下既可,修改代碼以下:

再次編譯運行:

關於這個緣由,以後會來解決,先佔且不關心,等一會就會正常了,正常運行的效果以下:

就實現了客戶端與服務器端的socket通信了,最終的代碼以下: echosrv.c【服務端】:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    char recvbuf[1024];//用來存儲客戶端發來的數據
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = read(conn, recvbuf, sizeof(recvbuf));//從套接字中讀取數據
        fputs(recvbuf, stdout);//打印到屏幕上
        write(conn, recvbuf, ret);//而且將其又回顯給客戶端,其第三個參數的長度正好是咱們接收到的長度
    }
    close(conn);
    close(listenfd);
    
    return 0;
}
複製代碼

echocli.c【客戶端】:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] ={0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        write(sock, sendbuf, strlen(sendbuf));
        read(sock, recvbuf, sizeof(recvbuf));

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock);
    
    return 0;
}
複製代碼

對於服務器端:echosrv.c

對於客戶端:echocli.c

下面經過一個簡單的圖來描述一下其關係:

可想而知,這兩個套接字都有本身的地址,對於conn服務端而言,它的地址是在綁定的時候確認的,也就是:

而對於sock客戶端而言,它的地址是在鏈接成功時肯定的。一旦鏈接成功,則會自動選擇一個本機的地址和端口號,當一個客戶端鏈接服務器成功時,在服務器端是能夠打印出客戶端的地址和端口信息的,具體代碼以下:

因此能夠將其客戶端的地址和端口號打印出來,修改服務端代碼以下:

此次編譯運行看下效果:

當客戶端鏈接成功時,則在服務端將其客戶端的ip和端口號打印出來了。

接下來,要解決一個上篇博文中遇到的問題,問題現象就是以下:

緣由是因爲,重啓時會從新再綁定,而此時該服務器是處於TIME_WAIT狀態,經過命令能夠查看到該狀態:

【注意】:TIME_WAIT狀態,需服務器與客戶端都已經退出來纔會出來。

而該狀態下,默認是沒法再次綁定的,那如何解決此問題呢?可使用SO_REUSEADDR選項來解決此問題:

具體使用方法以下,修改服務端代碼以下:

這時再來看是否解決了此問題:

可是,這個錯誤還會在一種場景下報出來,以下:

下面再來看一下目前程序的問題:目前服務器只能接收一個客戶端的鏈接,看下面:

分析一下服務端的代碼就能夠得知:

解決這個問題的思路就是:一個鏈接一個進程來處理併發(process-per-connection),也就是上面畫紅圈的放到子進程去處理,而後主進程能夠去accept客戶端的請求了,具體代碼修改以下:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void do_service(int conn)//將處理客戶端請求數據邏輯封裝到一個函數中,這樣代碼更加模塊化
{
    char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            fputs(recvbuf, stdout);
            write(conn, recvbuf, ret);
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)//用一個循環來不斷處理客戶端的請求,因此將accept操做也放到循環中
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();//建立進程
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {//子進程來處理與客戶端的數據
            close(listenfd);//對於子進程而言,監聽listenfd套接字沒用,則直接關閉掉,注意:套接字在父子進程是共享的
            do_service(conn);//開始處理請求數據
        }
        else//父進程則回到while循環頭,去accept新的客戶端的請求,這樣就比較好的解決多個客戶端的請求問題
            close(conn);
    }
    
    return 0;
}
複製代碼

下面來看下效果:

對於這段程序,其實還須要完善,也就是不能監聽客戶端的退出,

那怎麼監聽客戶端的監聽呢?

編譯運行看一下效果:

從中能夠看到,當客戶端退出時,服務端也收到消息了。

下面用多進程方式實現點對點聊天來進一步理解套接字編程,這裏實現的聊天程序是這樣:

那無論對於服務端,仍是客戶端,應該每一個端點都有有兩個進程,一個進程讀取對等方的數據,另外一個進程專門從鍵盤中接收數據發送給對待方,這裏實現的只是一個服務端跟一個客戶端的通信,不考慮一個服務端跟多個客戶端的通信了,重在練習,下面仍是基於以前的代碼開始實現:

p2psrv.c【點對點通訊服務端】:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));


    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");
    
    if (pid == 0)
    {//子進程用來從鍵盤中獲取輸入輸數據向客戶端發送數據
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        exit(EXIT_SUCCESS);
    }
    else
    {//父進程讀取從客戶端發送過來的數據
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}
複製代碼

基本上前面的listen、bind、accept的代碼沒動,編譯一下:

p2pcli.c【點對點通訊客戶端】:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)


int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");

    if (pid == 0)
    {//子進程接收來自服務端發來的數據
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(sock, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        close(sock);
    }
    else
    {//父進程獲取從鍵盤中敲入的命令向服務端發送數據
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        close(sock);
    }
    return 0;
}
複製代碼

編譯運行:

可見就實現了點對點的聊天程序。這時客戶端關閉時,服務端也關閉了,可是,實際上服務端的程序仍是有些問題的,分析一下代碼:

能夠在父子進程退出時都打印一個log來驗證下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));


    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");
    
    if (pid == 0)
    {
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        printf("child close\n");
        exit(EXIT_SUCCESS);
    }
    else
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        printf("parent close\n");
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}
複製代碼

而此時,若是我按任意一個字符,則子進程就退出來:

那怎麼解決當父進程退出時,也讓其子進程退出呢,就能夠用到咱們以前學過的信號來解決了,具體以下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void handler(int sig)//當子進程收到信號時,則將自身退出
{
    printf("recv a sig=%d\n", sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));


    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");
    
    if (pid == 0)
    {
        signal(SIGUSR1, handler);//子進程註冊一個SIGUSR1信號,以便在父進程退出時,通知子進程退出
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        printf("child close\n");
        exit(EXIT_SUCCESS);
    }
    else
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        printf("parent close\n");
        kill(pid, SIGUSR1);//當父進程退出時,發送一個SIGUSR1信號
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}
複製代碼

這時再看下效果:

若是反過來呢,將服務端關閉,那客戶端程序也會關閉麼?

能夠看到,當服務端退出時,客戶端並無退出,那是啥緣由呢?

那解決此問題也是能夠利用信號來解決,就像服務端同樣,能夠在子進程退出時,向父進程發送一個信號,而後父進程也退出既可,修改代碼以下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");

    if (pid == 0)
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(sock, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        close(sock);
        kill(getppid(), SIGUSR1);
    }
    else
    {
        signal(SIGUSR1, handler);
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        close(sock);
    }


    
    return 0;
}
複製代碼

這裏就很少解釋了,跟服務端的道理同樣,這樣再來運行一下看下效果:

流協議與粘包

關於什麼是粘包可能有些抽象,先得有一些理論基礎:咱們知道TCP是一個基於字節流的傳輸服務,這意味着TCP所傳輸的數據之間是無邊界的,像流水同樣,是沒法區分邊界的;而UDP是基於消息的傳輸服務,它傳輸的是數據報文,是有邊界的。

而對於數據之間有無邊界,反映在對方接收程序的時候,是不同的:對於TCP字節流來講,對等方在接收數據的時候,不可以保證一次讀操做,可以返回多少個字節,是一個消息,仍是二個消息,這些都是不肯定的;而對於UDP消息服務來講,它可以保證對等方一次讀操做返回的是一條消息。

因爲TCP的無邊界性,就會產生粘包問題,那粘包問題具體體現是怎樣的呢?下面用圖來進行闡述:

假設主機A(Host A)要向主機B(Host B)發送兩個數據包:M1,M2

而對於對待接收方主機B來講,可能會有如下幾種狀況:

也就是第一次讀操做恰好返回第一條消息(M1)的所有,接下來第二次讀操做返回第二條消息(M2)的所有,因此這就沒有粘包問題。

一次讀操做就返回了M1,M2的全部,這樣M1和M2就粘在一塊兒了,這就能比較直觀的體會到粘包的表現了。

一次讀操做返回了M1的所有,而且還有M2的一部分(m2_1);第二次讀操做返回了M2的另一部分(M2_2)。

一次讀操做返回了M1的一部分(M1_1);第二次讀操做返回了M1的另一部分(M1_2),而且還有M2的所有。

固然除了上面四種狀況,可能還存在其它組合,由於主機B一次能接收的字節數是不肯定的。

下面來探討下產生的緣由。

粘包產生的緣由

① 應用程要將本身緩衝區中的數據發送出去,首先要調用一個write方法,將應用程序的緩衝區的數據拷貝到套接口發送緩衝區(SO_SNDBUF),而該緩衝區有一個SO_SNDBUF大小的限制,若是應用緩衝區一條消息的大小超過了SO_SNDBUF的大小,那這時候就有可能產生粘包問題,由於消息被分隔了,一部分已經發送給發送緩衝區,且對方已經接收到了,另一部分才放到了發送緩衝區,這樣對方就延遲接收了消息的後一部分。這就致使了粘包問題的出現。

②TCP傳輸的段有最大段(MSS)的限制,因此也會對應用發送的消息進行分割而產生粘包問題。

③鏈路層它所傳輸的數據有一個最大傳輸單元(MTU)的限制,若是咱們所發送的數據包超過了最大傳輸單元,會在IP層進行分組,這也可能致使消息的分割,因此也有可能出現粘包問題。

固然還有其它緣由,如TCP的流量控制、擁塞控制、TCP的延遲發送機制,對於上面說的理論理解起來比較抽象,只要記住一條:TCP會產生粘包問題既可。

粘包解決方案

既然TCP協議沒有在傳輸層沒有維護消息與消息之間的邊界,因此:

咱們所要發送的消息是一個定長包,那麼對等方在接收的時候已定長的方式來進行接收,就能確保消息與消息之間的邊界。

這種方式有個問題,就是若是消息自己就帶這些字符的話,就沒法就沒法區分消息的邊界了,這時就須要用到轉義字符了。

其中包頭是定長的,如4個字節。

這些解決方案有一個很重要的問題,就是定長包的接收,咱們以前說了,TCP是一個流協議,它不能保證對方一次接收接收到了多少個字節,那咱們就須要封裝一個函數:接收肯定字節數的讀操做。

下面來封裝兩個函數,以下:

readn、writen

接收確切數目的讀操做

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void do_service(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(conn, recvbuf, sizeof(recvbuf));
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read");
                fputs(recvbuf, stdout);
                write(conn, recvbuf, ret);
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
複製代碼

echocli.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] ={0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        write(sock, sendbuf, strlen(sendbuf));
        read(sock, recvbuf, sizeof(recvbuf));

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock);
    
    return 0;
}
複製代碼

對於這個函數的封裝,仍是參考這個原形來設計,參數保持同樣:

這樣,最後用咱們寫的函數來替換這個系統調用既可,下面則正式開始封裝此函數:

ssize_t readn(int fd, void *buf, size_t count)//讀取count個字節數,其中size_t是無符號的整數,ssize_t是有符號的整數
{
    size_t nleft = count;//剩餘的字節數
    ssize_t nread;//已接收的字節數
    char *bufp = (char*)buf;

    while (nleft > 0)
    {//因爲不能保證一次讀操做可以返回字節數是多少,因此須要進行循環來接收
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)//被信號中斷了,則繼續執行,由於不是出錯
                continue;
            return -1;//表示讀取失敗了
        }
        else if (nread == 0)//對等方關閉了
            return count - nleft;//返回已經讀取的字節數

        bufp += nread;
        nleft -= nread;
    }

    return count;
}
複製代碼

【說明】:關於這個函數的編寫,能夠好好理解下,目的就是用咱們本身封裝的方法來代替系統的讀方法。

發送確切數目的寫操做

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)//若是是這種狀況,則表示什麼都沒發生,繼續還得執行
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}
複製代碼

接下來,用咱們本身封裝的函數來代碼系統函數,先只修改客戶端程序,一步步來引導其這樣作的緣由。

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

void do_service(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readn(conn, recvbuf, sizeof(recvbuf));//將其替換成本身封裝的方法
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read");
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, ret);
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
複製代碼

這時客戶端程序還保持原樣,這時編譯,那會有什麼效果呢:

發現,這時客戶端發送的數據服務端沒有辦法接收了,這是爲何呢?

若是對方發送數據不足1024個字節時,那就會一直循環,查看其readn函數:

這時,解決方案,第一種就是發送定長包:

因此,這時將客戶端的write替換成writen,以下:

echocli.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)//須要將函數的定義也挪過來
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] ={0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        writen(sock, sendbuf, sizeof(sendbuf));
        readn(sock, recvbuf, sizeof(recvbuf));

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock);
    
    return 0;
}
複製代碼

這時再運行,看問題是否解決:

這時就解決了以前的問題,可是有一個問題,每次發送都是1024定長的字節,若是隻發送幾個字節的內容也會佔用這麼多字節,這就會增長網絡的負擔,那怎麼解決這個問題呢?

這時候須要本身定義一個協議,能夠定義這樣一個包的結構:

這時,在發送數據時,就得進行相應的修改,以下:

這時,服務端接收數據時,也須要進行修改:

當服務端接收完以後,接着回顯給客戶端,代碼修改以下:

這時客戶端接收也同理,也是先讀取包長度,而後再接收包數據,修改以下:

至此,就已經將解決定長字長的問題的代碼寫完了,下面來編譯運行一下:

至此,這樣就很好的解決了粘包問題,在局域網中是不可能出現粘包問題的,可是若是將程序放到廣域網,若是不處理粘包問題會存在很大問題的。

對於以前寫的回射客戶/服務器端的程序中,咱們是用的read和write來讀取和發送數據的,以下:

那recv相對於read有什麼區別呢?先看一下man幫助:

其實它跟read函數功能同樣,均可以從套接口緩衝區sockfd中取數據到buf,可是recv僅僅只可以用於套接口IO,並不能用於文件IO以及其它的IO,而read函數能夠用於任何的IO;

recv函數相比read函數多了一個flags參數,經過這個參數能夠指定接收的行爲,比較有用的兩個選項是:

關於這個選項,先作下了解。

這個此次要學習的,它能夠接收緩衝區中的數據,可是並不從緩衝區中清除,這是跟read函數有區別的地方,read函數一旦讀取了,就會直接從緩衝區中清除。

下面用recv函數來封裝一個recv_peek函數,仍是繼上節中的程序進行修改:

注意:這時緩衝區中的數據還在,下面利用這個封裝的函數來實現readline。

也就是實現按行讀取,讀取直到\n字符,實際上,它也能解決上節中提到的粘包問題,回顧下上節的粘包問題解決方案:

咱們只要解釋\n爲止,表示前面是一個條合法的消息,對於readline的實現,能夠有三種方案:

①、最簡單的方案就是一個字符一個字符的讀取,而後作判斷是否有"\n",可是這種效率比較低,由於會屢次掉用read或recv系統函數。

②、用一個static變量保存接收到的數據進行緩存,在下次時從這個緩存變量中讀取而後估"\n"判斷。可是一旦用到了static變量,這意味着用到的函數是不可重錄函數

③、偷窺的方法,也就是此次要採用的方案。下面就利用咱們封裝的recv_peek函數實現readline:

首先用recv從緩衝區中偷窺數據:

接着判斷是否有"\n"字符,而後將其從緩衝區中讀出來,而且清空緩衝區:

若是沒有找到"\n"字符,則讀取數據並清空緩存,並不斷循環,具體以下:

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;//已讀字節數
    char *bufp = buf;
    int nleft = maxline;//剩餘字節數
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)//證實讀取失敗了
            return ret;
        else if (ret == 0)//證實是對方關閉了
            return ret;

        nread = ret;

        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);//因爲readn中是用的read函數讀取,因此讀取數據以後會將清空緩衝區,也正好須要這樣
                if (ret != i+1)//若是讀出來的字符數不等於i+1,則說明讀取失敗了
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)//這種狀況說明也是讀取有問題的
            exit(EXIT_FAILURE);

        //執行到此則說明沒有找到"\n"字符,這時讀取數據而後清空緩衝區
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;//再繼續讀後面的
    }

    return -1;//執行到此,則說明失敗了
}
複製代碼

下面則用這個readline方法來解決回射客戶/服務端粘包問題,因爲是按一行一行發送數據,說明消息之間的邊界就是"\n",因此對於以前封裝的定長包結構的方式能夠去掉了:

將服務端改爲按行讀取代碼以下:

對於客戶端修改也同理:

首先將咱們封裝的readline的兩個方法拷貝過來:

而後也改爲按行讀取:

最後來編譯運行:

可見,經過按行讀取的方式,也一樣達到了回射客戶/服務器端的效果,而且也解決了粘包問題。

下面貼出服務端與客戶端修改後的完整代碼:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;//已讀字節數
    char *bufp = buf;
    int nleft = maxline;//剩餘字節數
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)//證實讀取失敗了
            return ret;
        else if (ret == 0)//證實是對方關閉了
            return ret;

        nread = ret;

        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);//因爲readn中是用的read函數讀取,因此讀取數據以後會將清空緩衝區,也正好須要這樣
                if (ret != i+1)//若是讀出來的字符數不等於i+1,則說明讀取失敗了
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)//這種狀況說明也是讀取有問題的
            exit(EXIT_FAILURE);

        //執行到此則說明沒有找到"\n"字符,這時讀取數據而後清空緩衝區
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;//再繼續讀後面的
    }

    return -1;//執行到此,則說明失敗了
}

void do_service(int conn)
{
    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        fputs(recvbuf, stdout);
        writen(conn, recvbuf, strlen(recvbuf));
    }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
複製代碼

echocli.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;//已讀字節數
    char *bufp = buf;
    int nleft = maxline;//剩餘字節數
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)//證實讀取失敗了
            return ret;
        else if (ret == 0)//證實是對方關閉了
            return ret;

        nread = ret;

        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);//因爲readn中是用的read函數讀取,因此讀取數據以後會將清空緩衝區,也正好須要這樣
                if (ret != i+1)//若是讀出來的字符數不等於i+1,則說明讀取失敗了
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)//這種狀況說明也是讀取有問題的
            exit(EXIT_FAILURE);

        //執行到此則說明沒有找到"\n"字符,這時讀取數據而後清空緩衝區
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;//再繼續讀後面的
    }

    return -1;//執行到此,則說明失敗了
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        writen(sock, sendbuf, strlen(sendbuf));

        int ret = readline(sock, recvbuf, sizeof(recvbuf));
        if (ret == -1)
                ERR_EXIT("readline");
        else if (ret == 0)
        {
                printf("client close\n");
                break;
        }

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }
    
    return 0;
}
複製代碼

getsockname:獲取套接口本地的地址

當客戶端成功與服務端鏈接以後,若是想知道客戶端的地址,就能夠經過它來獲取,修改代碼以下:

而後編譯運行:

getpeername:獲取對等方的地址

因爲它的使用方法跟getsockname同樣,這裏就不說明了,注意:sockfd需是鏈接成功的套接口,另外對於服務端獲取客戶端ip,像這種狀況下也需用這個接口來得到:

gethostname:獲取主機的名稱

gethostbyname:經過主機名來獲取主機上全部的ip地址

下面利用上面的函數,來獲取主機上全部的ip地址:

編譯運行:

查看man:

因而加入該頭文件:

再次編譯:

可能本地ip列表有多個,可是通常來講默認本機ip都是第一個,因此,對於得到本機ip能夠將其封裝成一個方法,便於以後直接調用,以下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int getlocalip(char *ip)
{
    char host[100] = {0};
    if (gethostname(host, sizeof(host)) < 0)
      return -1;
    struct hostent *hp;
    if ((hp = gethostbyname(host)) == NULL)
      return -1;
    strcpy(ip, inet_ntoa(*(struct in_addr*)hp->h_addr_list[0]));
    return 0;

}

int main(void)
{
    char host[100] = {0};
    if (gethostname(host, sizeof(host)) < 0)
        ERR_EXIT("gethostname");

    struct hostent *hp;
    if ((hp = gethostbyname(host)) == NULL)
        ERR_EXIT("gethostbyname");

    int i = 0;
    while (hp->h_addr_list[i] != NULL)
    {
        printf("%s\n", inet_ntoa(*(struct in_addr*)hp->h_addr_list[i]));
        i++;
    }
    
    char ip[16] = {0};
    getlocalip(ip);
    printf("localip=%s\n", ip);
    return 0;
}
複製代碼

編譯運行:

另外,經過man幫助能夠查看到一點:

那獲得的信息就能夠將上面得到默認地址用它進行替換:

回顧一下咱們之間實如今TCP回射客戶/服務器程序,首先回顧一下第一個版本:

TCP客戶端從stdin獲取(fgets)一行數據,而後將這行數據發送(write)到TCP服務器端,這時TCP服務器調用read方法來接收而後再將數據回射(write)回來,客戶端收到(read)這一行,而後再將其輸出fputs標準輸出stdout,可是這個程序並無處理粘包問題,由於TCP是流協議,消息與消息之間是沒有邊界的,爲了解決這個問題,因而第二個改進版程序誕生了:

一行一行的發送數據,每一行都有一個\n字符,因此咱們在服務器端實現了按行讀取的readline方法,另外發送也並不能保證一次調用write方法就將tcp應用層的全部緩衝區拷貝到了套接口緩衝區,因此咱們封裝了一個writen方法進行一個更可靠消息的發送,因此就很好的解決了粘包問題。

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)
            return ret;
        else if (ret == 0)
            return ret;

        nread = ret;
        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)
            exit(EXIT_FAILURE);

        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
    }

    return -1;
}

void echo_srv(int conn)//因爲這個函數的意義就是回顯消息給客戶端,因此改一個函數名
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;

    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            echo_srv(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
複製代碼

echocli.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char*)buf;

        while (nleft > 0)
        {
                if ((nread = read(fd, bufp, nleft)) < 0)
                {
                        if (errno == EINTR)
                                continue;
                        return -1;
                }
                else if (nread == 0)
                        return count - nleft;

                bufp += nread;
                nleft -= nread;
        }

        return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char*)buf;

        while (nleft > 0)
        {
                if ((nwritten = write(fd, bufp, nleft)) < 0)
                {
                        if (errno == EINTR)
                                continue;
                        return -1;
                }
                else if (nwritten == 0)
                        continue;

                bufp += nwritten;
                nleft -= nwritten;
        }

        return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
        while (1)
        {
                int ret = recv(sockfd, buf, len, MSG_PEEK);
                if (ret == -1 && errno == EINTR)
                        continue;
                return ret;
        }
}


ssize_t readline(int sockfd, void *buf, size_t maxline)
{
        int ret;
        int nread;
        char *bufp = buf;
        int nleft = maxline;
        while (1)
        {
                ret = recv_peek(sockfd, bufp, nleft);
                if (ret < 0)
                        return ret;
                else if (ret == 0)
                        return ret;

                nread = ret;
                int i;
                for (i=0; i<nread; i++)
                {
                        if (bufp[i] == '\n')
                        {
                                ret = readn(sockfd, bufp, i+1);
                                if (ret != i+1)
                                        exit(EXIT_FAILURE);

                                return ret;
                        }
                }

                if (nread > nleft)
                        exit(EXIT_FAILURE);

                nleft -= nread;
                ret = readn(sockfd, bufp, nread);
                if (ret != nread)
                        exit(EXIT_FAILURE);

                bufp += nread;
        }

        return -1;
}

void echo_cli(int sock)//將以前這一段代碼封裝成一個函數,回顯客戶端,讓其代碼更加整潔。
{
    char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
                writen(sock, sendbuf, strlen(sendbuf));

                int ret = readline(sock, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                        ERR_EXIT("readline");
                else if (ret == 0)
                {
                        printf("client close\n");
                        break;
                }

                fputs(recvbuf, stdout);
                memset(sendbuf, 0, sizeof(sendbuf));
                memset(recvbuf, 0, sizeof(recvbuf));
        }

        close(sock);
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    struct sockaddr_in localaddr;
    socklen_t addrlen = sizeof(localaddr);
    if (getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
        ERR_EXIT("getsockname");

    printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));


    echo_cli(sock);

    return 0;
}
複製代碼

運行效果以下:

對於上面運行程序,當客戶端退出以後,服務端會產生僵進程:

對於僵進程的避勉,有以下方法,以前都有介紹過:

忽略SIGCHLD信號,因此,能夠在服務端加入:

編譯運行,看是否解決了僵進程的問題:

第二種方式,則能夠捕捉SIGCHLD信號來進行忽略,具體代碼以下:

此時再編譯運行:

可是,對於上面兩種解決方式仍是會存在一些問題,若是有不少個子進程同時退出,wait函數並不能等待全部子進程的退出,由於wait僅僅只等待第一個子進程退出就返回了,這時就須要用到waitpid了,在這以前,須要模擬一下由五個客戶端併發鏈接至服務器,並同時退出的狀況,用簡單示例圖來描述以下:

客戶端建立五個套接字來鏈接服務器,一旦鏈接了服務器就會建立一個子進程出來爲客戶端進行處理,從圖中能夠看,服務端建立了5個子進程出來。

對於服務端程序是不須要進行修改的,只需修改客戶端建立五個套接字既可,修改客戶端程序以下:

這時,編譯運行:

關於這個比較容易理解,是因爲wait函數只等待一個子進程退出就返回了,因此有四個進程處於殭屍的狀態。

再運行一次:

那若是再運行一次呢?

從以上結果來看,殭屍進程的數目不必定。

因爲wait函數只能返回一個子進程,這時候咱們應該用什麼方法解決呢?能夠用waitpid函數來解決,具體程序修改以下:

再來運行看是否還存在殭屍進程呢?

這種狀況有一點不太好解釋,可是還會遇到這種狀況:

對於這種狀況,就能夠用理論來解釋了,緣由是因爲有五個子進程都要退出,這就意味着有五個信號要發送給父進程,

在關閉鏈接時,會向服務器父進程發送SIGCHLD信號,具體以下圖:

此時父進程處於一個handle_sigchld()的過程,而在這個處理過程當中,若是其它信號到了,其它信號會丟失,以前也提到過,這些信號是不可靠信號,不可靠信號只會排隊一個,若是隻排隊一個的話,那麼最終可以處理兩個子進程,因此,最後存在三個殭屍進程。

那爲何有時會有2個僵進程,有時又會有四個呢,這裏來解釋一下:

緣由可能跟FIN(客戶端在終止時,會向服務器發送FIN)到達的時機有關,服務器收到FIN的時候,返回等於0就退出了子進程,這時就會向服務器發送SIGCHLD信號,若是這些信號都是同時到達的話,那麼就有可能只處理一個,這時就會出現了4個殭屍進程;若是不是同時到達,handle_sigchld()函數就會執行屢次,若是被執行了兩次,捕捉到了兩個信號的話,那就此時就會出現3個僵進程,以此類推,但無論結果怎樣,都是屬於須要解決的狀況。

那怎麼解決此問題呢,能夠用一個循環來作:

這時再編譯運行:

經過這個狀態的學習,進一步複習一下「鏈接創建三次握手、鏈接終止四次握手【下面會分別來介紹】」,下面首先來看一張圖:

從圖中能夠數一下,總共有「LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_一、CLOSE_WAIT、FIN_WAIT_二、LAST_ACK、TIME_WAIT、CLOSED」十個狀態,那爲啥標題上說有十一個呢?其實還有一個狀態叫CLOSING,這個狀態產生的緣由比較特珠,咱們以後再來看它,下面先來分別梳理一下鏈接創建三次握手和鏈接終止四次握手狀態流程:

鏈接創建三次握手

LISTEN

首先服務端建立一個socket,這時它的狀態其實是CLOSED狀態,也就是最後一種狀態,雖然沒有標識出來:

一旦咱們調用bind、listen函數:

這時就處於LISTEN狀態,以下:

這時候的套接口就稱爲被動套接口,這意味着這個套接口不能用於發起鏈接,只能用來接受鏈接,這個以前都有介紹,

而這時回到客戶端來講:

SYN_SENT:

當建立套接口時,也是一個CLOSED狀態,這裏也未標明,接着再調用connect進行主動打開,這時的套接口就稱爲主動套接口,它能夠用來發起鏈接的:

這時的狀態爲SYN_SENT,這時TCP會傳輸一個發起鏈接的TCP段"SYN a"這個段給服務器端,以下:

而此時服務端調用了accept方法處理阻塞的狀態:

可是TCP協議棧會收到「SYN a」TCP段,這時就處於SYN_RCVD狀態:

當收到SYN_RCVD以後,TCP會對序號「SYN a」進行確認,發起一個"ACK a+1"TCP段給客戶端,而且也有一個"SYN b"序號,以下:

對於客戶端來講,收到"ACK a+1"TCP段以後,就處於"ESTABLISHED"鏈接的狀態,這時connect函數就可以返回;

而對於服務端來講並未處理鏈接的狀態,它須要等到客戶端再次發送"ACK b+1"TCP段,這就是鏈接創建的三次握手,服務端收到這個TCP段以後,則也會處於"ESTABLISHED"狀態,以下:

它實際上會將未鏈接隊列當中的一個條目移至已鏈接隊列當中,這時accept就能夠返回了,由於它能夠從已鏈接隊列的隊頭返回第一個鏈接,以下:

鏈接終止四次握手

當客戶端發起關閉請求,這時會向服務器端發起一個"FIN x ACK y"的TCP段給對方,這時客戶端的狀態就叫做FIN_WAIT_1,以下:

這時服務器端收到一個終止的TCP段,這時read就會返回爲0,實際上當服務端收到這個TCP段以後,服務端會對它進行確認,則會向客戶端發送"ACK+1"的TCP段,這時服務端的狀態爲CLOSE_WAIT:

客戶端的狀態爲FIN_WAIT_2:

這時候,客戶端就處於TIME_WAIT狀態:

注意:這個狀態要保留2倍的MSL(tcp最大的生命期)時間,爲何呢,這個能夠簡單說明一下,是因爲最後一個"ACK y+1"發送過去,不能肯定對方收到了,這個ACK可能會丟失,有了這個時間的存在就確保了能夠重傳ACK,固然還有其它的緣由,這裏先了解一下既可,當服務器收到了最後一個確認之後,則就處於CLOSED狀態了:

注意:服務端處於CLOSED狀態,並不表明發送關閉的這一端(就是客戶端)就處於CLOSED狀態,需等到2倍的MSL時間消失之後才處理CLOSED狀態。

以上是TCP的十點狀態,還有一個特珠狀態叫CLOSING,它產生的緣由是:雙方同時關閉,以下圖:

具體流程是這樣的:客戶端和服務端同時調用close,這時客戶端和服務端都處於FIN_WAIT_1狀態

這時,雙方都會發起FIN TCP段,

這時須要對其進行段確認:

這時狀態則稱爲CLOSING狀態,這時就不會進行到FIN_WAIT_2這種狀態了。

一旦收到對方的ACK,則會處於TIME_WAIT狀態:

可見TIME_WAIT狀態是主動關閉的一方纔產生的狀態。

說了這麼多理論,下面用代碼來進行論證,以便增強理解,仍是用以前的服務端/客戶端回顯的例子,首先啓動服務端,這時查看下狀態:

接着啓動一個客戶端,發起鏈接:

因爲目前作實驗是在同一臺機器上進行的,因此打印了三個狀態,實際上應該是服務端的狀態和客戶端的狀態是分開的。

【注意】:因爲在運行時這兩個狀態SYN_SENT、SYN_RCVD過快,因此看不到。

下面來看下鏈接終止的狀態,先關閉服務端,首先找到服務端的進程,經過kill掉的辦法來關閉服務端:

殺掉服務端進程來模擬服務端的close:

【注意】:這裏的服務端進程是指與客戶端通信的進程。

這時查看一下狀態:

爲何不會處於TIME_WAIT狀態呢?

緣由在於,read函數沒有機會返回0:

這時應該查看一下客戶端的程序才知道問題,客戶端此時是阻塞在fgets函數來鍵盤的消息:

這就意味着客戶端這個進程沒有機會調用close,因此服務器端沒法進入TIME_WAIT,它只能保留在FIN_WAIT_2狀態了

這時候再來看一下狀態:

只有LISTEN狀態了,這是爲何呢?

仍是得回到客戶端的程序來分析,因爲從鍵盤敲入了字符,因此:

這時就會走以下流程:

而因爲服務器的進程已經殺掉了,因此說不會顯示TIME_WAIT狀態了。

【注意】:該實驗在最後會闡述一個SIGPIPE的信號問題。

若是先關閉客戶端,這時就會看到這個狀態了,以下:

另外這個狀態上面也提到了,會保留2倍的MSL時間纔會消失,若是服務器端保留兩倍的MSL時間,這時候就會致使服務器端沒法從新啓動,若是沒有調用SO_REUSEADDR話。

SIGPIPE信號產生的緣由

對於上面作的一個實驗,就是服務端先關閉以後,而後客戶端還能夠向服務端發起數據,這是因爲客戶端收到FIN僅僅表明服務端不能發送數據了,以下圖:

而若是發送數據給對方,可是對方進程又已經不存在,會致使對方發送一個RST重啓TCP段給發送方(這裏指的就是上面作實驗的客戶端),可是在收到RST段以後,若是再調用write就會產生SIGPIPE信號,而產生該信號默認就會終止程序,下面來修改一下客戶端的代碼,仍是基於上面的實驗,以下:

這時,再來看一下效果:

首先運行客戶端與服務端:

而後將服務端與客戶端的進程找到,並殺掉來模擬關閉服務端:

而後這時在客戶端中敲入字符,並回車,看下結果:

結合代碼來看一下:

爲了證實確實是收到了SIGPIPE信號,咱們捕捉一下該信號,修改代碼以下:

再次編譯運行,這一次運行步驟還跟上次同樣,須要先殺掉父進程,而後再在客戶端敲入一個字符,這裏就不說明了,只看一下結果:

實際上,對於這個信號的處理咱們一般忽略便可,能夠加入這條語句:signal(SIGPIPE, SIG_IGN);

修改代碼以下:

實際上,對於SIGPIPE信號在學習管道時有說過它的產生,若是沒有任何讀端進程,而後往管道當中寫入數據,這時候就會出現段開的管道,而對於TCP咱們能夠當作是一個全雙工的管道,當某一端收到FIN以後,它並不能確認對等方的進程是否已經消失了,由於對方調用了close並不意味着對進程就已經消失了,用圖來理解:

這時候,就須要客戶調用一次write,此次並不會產生斷開的管道,發現對等方的進程不存在了,則對等方就會發送RST段給客戶端,這就意味着全雙工管道的讀端進程不存在了,因此說若是再次調用write,就會致使SIGPIPE信號的產生,因此說能夠利用管道來理解它。

相關文章
相關標籤/搜索