国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP)

這篇具有很好參考價值的文章主要介紹了【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

目錄

地址轉(zhuǎn)換函數(shù)

字符串IP轉(zhuǎn)整數(shù)IP

整數(shù)IP轉(zhuǎn)字符串IP

關(guān)于inet_ntoa

簡單的單執(zhí)行流TCP網(wǎng)絡(luò)程序

TCP socket API 詳解及封裝TCP socket?

服務(wù)端創(chuàng)建套接字?

服務(wù)端綁定?

服務(wù)端監(jiān)聽?

服務(wù)端獲取連接?

服務(wù)端處理請求

客戶端創(chuàng)建套接字

客戶端連接服務(wù)器

客戶端發(fā)起請求

服務(wù)器測試

單執(zhí)行流服務(wù)器的弊端

多進程的TCP網(wǎng)絡(luò)程序

多級創(chuàng)建子進程

捕捉SIGCHLD信號

多線程的TCP網(wǎng)絡(luò)程序

線程池的TCP網(wǎng)絡(luò)程序?


地址轉(zhuǎn)換函數(shù)

字符串IP轉(zhuǎn)整數(shù)IP

本節(jié)只介紹基于IPv4的socket網(wǎng)絡(luò)編程,sockaddr_in中的成員struct in_addr sin_addr表示32位的IP 地址。但是我們通常用點分十進制的字符串表示IP 地址,以下函數(shù)可以在字符串表示和in_addr表示之間轉(zhuǎn)換;

字符串轉(zhuǎn)in_addr(整數(shù)ip)的函數(shù):

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

  • inet_aton函數(shù)的函數(shù)原型如下:?
int inet_aton(const char *cp, struct in_addr *inp);

參數(shù)說明:

  • cp:待轉(zhuǎn)換的字符串IP。
  • inp:轉(zhuǎn)換后的整數(shù)IP,這是一個輸出型參數(shù)。

返回值說明:

  • 如果轉(zhuǎn)換成功則返回一個非零值,如果輸入的地址不正確則返回零值。
  • inet_addr函數(shù)的函數(shù)原型如下:
in_addr_t inet_addr(const char *cp);

參數(shù)說明:

  • cp:待轉(zhuǎn)換的字符串IP。

返回值說明:

  • 如果輸入的地址有效,則返回轉(zhuǎn)換后的整數(shù)IP;如果輸入的地址無效,則返回INADDR_NONE(通常為-1)。
  • inet_pton函數(shù)的函數(shù)原型如下:?
int inet_pton(int af, const char *src, void *dst);

參數(shù)說明:

  • af:協(xié)議家族。
  • src:待轉(zhuǎn)換的字符串IP。
  • dst:轉(zhuǎn)換后的整數(shù)IP,這是一個輸出型參數(shù)。

返回值說明:

  • 如果轉(zhuǎn)換成功,則返回1。
  • 如果輸入的字符串IP無效,則返回0。
  • 如果輸入的協(xié)議家族af無效,則返回-1,并將errno設(shè)置為EAFNOSUPPORT。

整數(shù)IP轉(zhuǎn)字符串IP

in_addr轉(zhuǎn)字符串的函數(shù):

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

  • inet_ntoa函數(shù)的函數(shù)原型如下:?
char *inet_ntoa(struct in_addr in);

參數(shù)說明:

  • in:待轉(zhuǎn)換的整數(shù)IP。

返回值說明:

  • 返回轉(zhuǎn)換后的字符串IP。
  • inet_ntop函數(shù)的函數(shù)原型如下:?
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

參數(shù)說明:

  • af:協(xié)議家族。
  • src:待轉(zhuǎn)換的整數(shù)IP。
  • dst:轉(zhuǎn)換后的字符串IP,這是一個輸出型參數(shù)。
  • size:用于指明dst中可用的字節(jié)數(shù)。

返回值說明:

  • 如果轉(zhuǎn)換成功,則返回一個指向dst的非空指針;如果轉(zhuǎn)換失敗,則返回NULL。

代碼示例:

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

  • 我們最常用的兩個轉(zhuǎn)換函數(shù)是inet_addr和inet_ntoa,因為這兩個函數(shù)足夠簡單。這兩個函數(shù)的參數(shù)就是需要轉(zhuǎn)換的字符串IP或整數(shù)IP,而這兩個函數(shù)的返回值就是對應(yīng)的整數(shù)IP和字符串IP。
  • 其中inet_pton和inet_ntop不僅可以轉(zhuǎn)換IPv4的in_addr,還可以轉(zhuǎn)換IPv6的in6_addr,因此函數(shù)接口是void *addrptr。?
  • 實際這些轉(zhuǎn)換函數(shù)都是為了滿足某些打印場景的,除此之外,更多的是用來做某些數(shù)據(jù)分析,比如網(wǎng)絡(luò)安全方面的數(shù)據(jù)分析。

關(guān)于inet_ntoa

inet_ntoa這個函數(shù)返回了一個char*,很顯然是這個函數(shù)自己在內(nèi)部為我們申請了一塊內(nèi)存來保存ip的結(jié)果。那么是否需要調(diào)用者手動釋放呢?

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

man手冊上說,inet_ntoa函數(shù),是把這個返回結(jié)果放到了靜態(tài)存儲區(qū)。這個時候不需要我們手動進行釋放。
那么問題來了, 如果我們調(diào)用多次這個函數(shù), 會有什么樣的效果呢? 參見如下代碼:?

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器運行結(jié)果如下:?

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

因為inet_ntoa把結(jié)果放到自己內(nèi)部的一個靜態(tài)存儲區(qū), 這樣第二次調(diào)用時的結(jié)果會覆蓋掉上一次的結(jié)果.?

如果有多個線程調(diào)用 inet_ntoa, 是否會出現(xiàn)異常情況呢?

inet_ntoa函數(shù)內(nèi)部只在靜態(tài)存儲區(qū)申請了一塊區(qū)域,用于存儲轉(zhuǎn)換后的字符串IP,那么在線程場景下這塊區(qū)域就叫做臨界區(qū),多線程在不加鎖的情況下同時訪問臨界區(qū)必然會出現(xiàn)異常情況。并且在APUE中,也明確提出inet_ntoa不是線程安全的函數(shù)。

多線程調(diào)用inet_ntoa函數(shù)測試:?

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void* Func1(void* p) {
    struct sockaddr_in* addr = (struct sockaddr_in*)p;
    while (1) {
        char* ptr = inet_ntoa(addr->sin_addr);
        printf("addr1: %s\n", ptr);
    }
    return NULL;
}
void* Func2(void* p) {
    struct sockaddr_in* addr = (struct sockaddr_in*)p;
    while (1) {
        char* ptr = inet_ntoa(addr->sin_addr);
        printf("addr2: %s\n", ptr);
    }
    return NULL;
}
int main() {
    pthread_t tid1 = 0;
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    pthread_create(&tid1, NULL, Func1, &addr1);
    pthread_t tid2 = 0;
    pthread_create(&tid2, NULL, Func2, &addr2);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

但是實際在centos7上測試時,在多線程場景下調(diào)用inet_ntoa函數(shù)并沒有出現(xiàn)問題,可能是該函數(shù)內(nèi)部的實現(xiàn)加了互斥鎖,這就跟接口本身的設(shè)計也是有關(guān)系的。?

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

在多線程環(huán)境下, 推薦使用inet_ntop, 這個函數(shù)由調(diào)用者提供一個緩沖區(qū)保存結(jié)果, 可以規(guī)避線程安全問題;?

簡單的單執(zhí)行流TCP網(wǎng)絡(luò)程序

TCP socket API 詳解及封裝TCP socket?

socket()函數(shù):?

我們用man指令查看一下?socket()函數(shù):

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

  • socket()打開一個網(wǎng)絡(luò)通訊端口,如果成功的話,就像open()一樣返回一個文件描述符;
  • 應(yīng)用程序可以像讀寫文件一樣用read/write在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù);
  • 如果socket()調(diào)用出錯則返回-1;
  • 對于IPv4,family參數(shù)指定為AF_INET;
  • 對于TCP協(xié)議,type參數(shù)指定為SOCK_STREAM,表示面向流的傳輸協(xié)議
  • protocol參數(shù)的介紹從略,指定為0即可。?

服務(wù)端創(chuàng)建套接字?

下面我們將TCP服務(wù)器封裝成一個類,當(dāng)我們定義出一個服務(wù)器對象后需要對服務(wù)器進行初始化,而初始化TCP服務(wù)器首先要執(zhí)行的操作是創(chuàng)建一個套接字。

TCP服務(wù)器在調(diào)用socket函數(shù)創(chuàng)建套接字時,參數(shù)設(shè)置如下:

  • 協(xié)議家族選擇AF_INET,因為我們要進行的是網(wǎng)絡(luò)通信。
  • 創(chuàng)建套接字時所需的服務(wù)類型應(yīng)該是SOCK_STREAM,因為我們編寫的是TCP服務(wù)器,SOCK_STREAM提供的就是一個有序的、可靠的、全雙工的、基于連接的流式服務(wù)。
  • 協(xié)議類型默認(rèn)設(shè)置為0即可。
  • 如果創(chuàng)建套接字后獲得的文件描述符是小于0的,說明套接字創(chuàng)建失敗,此時也就沒必要進行后續(xù)操作了,直接終止程序即可。
const int defaultfd = -1;
class TcpServer
{
public:
    void InitServer()
    {
        //創(chuàng)建套接字
        listensock_ = socket(AF_INET,SOCK_STREAM,0);
        if(listensock_ < 0)
        {
           lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
           exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);
    }

    ~TcpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }

private:
    int listensock_;
};

在創(chuàng)建實際TCP服務(wù)器和UDP服務(wù)器的套接字時,方法基本相同。但在選擇服務(wù)類型時,TCP需要流式服務(wù),而UDP則需要用戶數(shù)據(jù)報服務(wù)。當(dāng)析構(gòu)服務(wù)器時,可以將服務(wù)器對應(yīng)的文件描述符進行關(guān)閉。

這里我們將套接字名字設(shè)置為listensock_,我們在編寫服務(wù)端監(jiān)聽的時候再進行說明

bind():?【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

  • 服務(wù)器程序所監(jiān)聽的網(wǎng)絡(luò)地址和端口號通常是固定不變的,客戶端程序得知服務(wù)器程序的地址和端口號后就可以向服務(wù)器發(fā)起連接;服務(wù)器需要調(diào)用bind綁定一個固定的網(wǎng)絡(luò)地址和端口號;
  • bind()成功返回0,失敗返回-1。
  • bind()的作用是將參數(shù)sockfd和myaddr綁定在一起,使sockfd這個用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽myaddr所描述的地址和端口號;
  • 前面講過,struct sockaddr *是一個通用指針類型,myaddr參數(shù)實際上可以接受多種協(xié)議的sockaddr結(jié)構(gòu)體,而它們的長度各不相同,所以需要第三個參數(shù)addrlen指定結(jié)構(gòu)體的長度;

我們的程序中對myaddr參數(shù)是這樣初始化的:

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

1. 將整個結(jié)構(gòu)體清零;
2. 設(shè)置地址類型為AF_INET;
3. 網(wǎng)絡(luò)地址為INADDR_ANY,這個宏表示本地的任意IP地址,因為服務(wù)器可能有多個網(wǎng)卡,每個網(wǎng)卡也可能綁定多個IP地址,這樣設(shè)置可以在所有的IP地址上監(jiān)聽,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址;
4. 端口號為SERV_PORT,我們定義為9999;?

服務(wù)端綁定?

套接字創(chuàng)建完畢后我們實際只是在系統(tǒng)層面上打開了一個文件,該文件還沒有與網(wǎng)絡(luò)關(guān)聯(lián)起來,因此創(chuàng)建完套接字后我們還需要調(diào)用bind函數(shù)進行綁定操作。

需要執(zhí)行以下綁定步驟:

  • 首先,定義一個struct sockaddr_in結(jié)構(gòu)體,用于存儲服務(wù)器網(wǎng)絡(luò)相關(guān)的屬性信息,如協(xié)議家族、IP地址和端口號。
  • 在填充這些信息時,協(xié)議家族應(yīng)設(shè)置為AF_INET,以表示使用IPv4協(xié)議。端口號是TCP服務(wù)器程序所使用的端口,需要在設(shè)置時使用htons函數(shù)將其從主機字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序。
  • 服務(wù)器的IP地址可以選擇設(shè)置為本地環(huán)回地址127.0.0.1,表示僅支持本地通信。若要支持網(wǎng)絡(luò)通信,可以將服務(wù)器的IP地址設(shè)置為公網(wǎng)IP地址。
  • 對于云服務(wù)器環(huán)境,在設(shè)置IP地址時,不需要顯式綁定特定的IP地址??梢詫P地址設(shè)置為INADDR_ANY,表示服務(wù)器可以從本地任何一張網(wǎng)卡上接收數(shù)據(jù)。由于INADDR_ANY本質(zhì)上表示0,因此在設(shè)置時不需要進行網(wǎng)絡(luò)字節(jié)序的轉(zhuǎn)換。
  • 完成服務(wù)器網(wǎng)絡(luò)屬性的設(shè)置后,需要調(diào)用bind函數(shù)進行綁定操作。綁定是將文件描述符與網(wǎng)絡(luò)地址關(guān)聯(lián)的過程。如果綁定失敗,則沒有必要繼續(xù)執(zhí)行后續(xù)操作,可以直接終止程序。
  • 由于TCP服務(wù)器在初始化時需要指定端口號,因此在服務(wù)器類中需要包含端口號成員變量。在實例化服務(wù)器對象時,需要向其傳入一個端口號。由于我當(dāng)前使用的是云服務(wù)器環(huán)境,因此在綁定TCP服務(wù)器的IP地址時,不需要顯式綁定公網(wǎng)IP地址,可以直接使用INADDR_ANY進行綁定。因此,我在服務(wù)器類中沒有引入IP地址成員變量。

由于TCP服務(wù)器初始化時需要服務(wù)器的端口號,因此在服務(wù)器類當(dāng)中需要引入端口號,當(dāng)實例化服務(wù)器對象時就需要給傳入一個端口號。而由于我當(dāng)前使用的是云服務(wù)器,因此在綁定TCP服務(wù)器的IP地址時不需要綁定公網(wǎng)IP地址,直接綁定INADDR_ANY即可,因此我這里沒有在服務(wù)器類當(dāng)中引入IP地址。?

const int defaultfd = -1;
const string defaultip = "0.0.0.0";
class TcpServer
{
public:
    TcpServer(const int& serverport,const string& ip = defaultip)
        :listensock_(defaultfd),
        port(serverport)
        ip_(ip)
    {}
    void InitServer()
    {
        //1.創(chuàng)建套接字
        listensock_ = socket(AF_INET,SOCK_STREAM,0);
        if(listensock_ < 0)
        {
           lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
           exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);

        //2.綁定
        struct sockaddr_in local;
        bzero(&local,sizeof(0));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_aton(INADDR_ANY,&(local.sin_addr));//換個接口寫一下

        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg(Info, "bind socket success, listensock_: %d", listensock_);
    }

    ~TcpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }

private:
    int listensock_;
    uint16_t port;
};

注意:

當(dāng)定義好struct sockaddr_in結(jié)構(gòu)體后,最好先用memset函數(shù)對該結(jié)構(gòu)體進行清空,也可以用bzero函數(shù)進行清空。bzero函數(shù)也可以對特定的一塊內(nèi)存區(qū)域進行清空。

TCP服務(wù)器綁定時的步驟與UDP服務(wù)器是完全一樣的,沒有任何區(qū)別。?

服務(wù)端監(jiān)聽?

UDP服務(wù)器的初始化步驟較為簡單,主要包括創(chuàng)建套接字和綁定。相比之下,TCP服務(wù)器需要進行更復(fù)雜的操作。由于TCP服務(wù)器是面向連接的,客戶端在發(fā)送數(shù)據(jù)之前需要先與服務(wù)器建立連接。因此,TCP服務(wù)器需要能夠監(jiān)聽客戶端的連接請求。為了實現(xiàn)這一功能,需要將TCP服務(wù)器創(chuàng)建的套接字設(shè)置為監(jiān)聽狀態(tài),以便等待和處理客戶端的連接請求。

listen()函數(shù):

設(shè)置套接字為監(jiān)聽狀態(tài)的函數(shù)叫做listen,該函數(shù)的函數(shù)原型如下:

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

參數(shù)說明:

  • sockfd:需要設(shè)置為監(jiān)聽狀態(tài)的套接字對應(yīng)的文件描述符。
  • backlog:全連接隊列的最大長度。如果有多個客戶端同時發(fā)來連接請求,此時未被服務(wù)器處理的連接就會放入連接隊列,該參數(shù)代表的就是這個全連接隊列的最大長度,一般不要設(shè)置太大,設(shè)置為5或10即可。

返回值說明:

  • 監(jiān)聽成功返回0,監(jiān)聽失敗返回-1,同時錯誤碼會被設(shè)置。

服務(wù)器監(jiān)聽

在完成套接字創(chuàng)建和綁定之后,TCP服務(wù)器需要進一步配置以監(jiān)聽新的連接請求。這是通過將套接字設(shè)置為監(jiān)聽模式來實現(xiàn)的,這樣服務(wù)器就可以等待并處理來自客戶端的連接請求。

如果監(jiān)聽操作失敗,說明服務(wù)器無法正常接收客戶端的連接請求。在這種情況下,沒有必要進行后續(xù)的操作,因為監(jiān)聽失敗意味著服務(wù)器無法正常工作。因此,當(dāng)監(jiān)聽失敗時,應(yīng)當(dāng)直接終止程序的執(zhí)行。?

const int defaultfd = -1;
const int backlog = 10; // 但是一般不要設(shè)置的太大
const string defaultip = "0.0.0.0";
class TcpServer
{
public:
    TcpServer(const int& serverport,const string& ip = defaultip)
        :listensock_(defaultfd),
        port(serverport),
        ip_(ip)
    {}
    void InitServer()
    {
        //1.創(chuàng)建套接字
        listensock_ = socket(AF_INET,SOCK_STREAM,0);
        if(listensock_ < 0)
        {
           lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
           exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);

        //2.綁定
        struct sockaddr_in local;
        bzero(&local,sizeof(0));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_aton(INADDR_ANY,&(local.sin_addr));//換個接口寫一下

        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg(Info, "bind socket success, listensock_: %d", listensock_);
        
        //服務(wù)端監(jiān)聽
        if(listen(listensock_,backlog) < 0)
        {
            lg(Fatal,"listen error, error: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }
        lg(Info, "listen socket success, listensock_: %d", listensock_);
    }

    ~TcpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }

private:
    int listensock_;
    uint16_t port;
};

在初始化TCP服務(wù)器時,創(chuàng)建的套接字不僅僅是一個普通的套接字,它被特別地稱為“監(jiān)聽套接字”為了使代碼更具描述性,我們將套接字的變量名設(shè)置為“l(fā)istensock_”。

TCP服務(wù)器的初始化過程要求套接字的創(chuàng)建、綁定和監(jiān)聽都成功完成。只有當(dāng)這三個步驟都順利執(zhí)行,TCP服務(wù)器的初始化才算完成。

服務(wù)端獲取連接?

TCP服務(wù)器初始化后就可以開始運行了,但TCP服務(wù)器在與客戶端進行網(wǎng)絡(luò)通信之前,服務(wù)器需要先獲取到客戶端的連接請求。

accept函數(shù)

獲取連接的函數(shù)叫做accept,該函數(shù)的函數(shù)原型如下:

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

函數(shù)說明:

  • 三次握手完成后, 服務(wù)器調(diào)用accept()接受連接;
  • 如果服務(wù)器調(diào)用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來;

參數(shù)說明:

  • sockfd:特定的監(jiān)聽套接字,表示從該監(jiān)聽套接字中獲取連接。
  • addr:addr是一個傳出參數(shù),accept()返回時傳出客戶端的網(wǎng)絡(luò)相關(guān)的屬性信息、協(xié)議家族、地址和端口號;
  • addrlen:是一個輸入輸出型參數(shù)。傳入的是調(diào)用者提供的,緩沖區(qū)addr的長度以避免緩沖區(qū)溢出問題,傳出的是客戶端地址結(jié)構(gòu)體的實際長度(有可能沒有占滿調(diào)用者提供的緩沖區(qū));

返回值說明:

  • 獲取連接成功返回接收到的套接字的文件描述符,獲取連接失敗返回-1,同時錯誤碼會被設(shè)置。

我們的服務(wù)器程序結(jié)構(gòu)是這樣的:

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

如何理解accept函數(shù)返回的套接字??

當(dāng)調(diào)用accept函數(shù)以獲取連接時,它從監(jiān)聽套接字中獲取連接。如果accept函數(shù)成功獲取連接,它會返回接收到的套接字對應(yīng)的文件描述符。

關(guān)于監(jiān)聽套接字與accept函數(shù)返回的套接字的作用:

  • 監(jiān)聽套接字:主要用于接收客戶端發(fā)來的連接請求。accept函數(shù)會持續(xù)從監(jiān)聽套接字中獲取新的連接。
  • accept函數(shù)返回的套接字:用于為本次獲取到的連接提供服務(wù)。監(jiān)聽套接字的主要任務(wù)是持續(xù)接收新的連接,而真正為這些連接提供服務(wù)的套接字是accept函數(shù)返回的套接字,而不是監(jiān)聽套接字。

總結(jié):監(jiān)聽套接字主要負責(zé)等待和接收客戶端的連接請求,而accept函數(shù)返回的套接字則負責(zé)為每個成功建立的連接提供服務(wù)。

舉個例子來理解這個過程:

我們?nèi)ド虉龀燥埖臅r候,經(jīng)過飯店門口,都會有一個人在外面拿著菜單進行拉客,店員把客人拉進飯店,他的任務(wù)就算是完成了,接下來真正為為客人進行服務(wù)的是廚師。

這個過程中,拉客的店員就好比監(jiān)聽套接字,而accept函數(shù)返回的套接字就好比廚師。

服務(wù)端獲取連接

在服務(wù)端處理連接時,需要注意以下幾點:

  • 首先,accept函數(shù)在獲取連接時可能會失敗。然而,TCP服務(wù)器不會因為某個連接獲取失敗而終止。因此,當(dāng)服務(wù)端獲取連接失敗時,應(yīng)該繼續(xù)嘗試獲取其他連接。
  • 其次,如果需要將獲取到的連接對應(yīng)的客戶端IP地址和端口號信息輸出,可以使用inet_ntoa函數(shù)將整數(shù)值的IP地址轉(zhuǎn)換為字符串表示。同時,使用ntohs函數(shù)將端口號從網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為主機字節(jié)序。
  • 值得注意的是,inet_ntoa函數(shù)在底層完成了兩個任務(wù):一是將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為主機字節(jié)序,二是將主機字節(jié)序的整數(shù)值IP地址轉(zhuǎn)換為點分十進制的字符串表示。
enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

class TcpServer
{
public:

    void Start()
    {
        lg(Info,"tcpServer is running...");
        for(;;)
        {
            //1.獲取新連接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if(sockfd < 0)
            {
                lg(Warning,"accept error, errno: %d ,errstring: %s", errno, strerror(errno));
                continue;//TCP服務(wù)器不會因為某個連接獲取失敗而終止。因此,當(dāng)服務(wù)端獲取連接失敗時,應(yīng)該繼續(xù)嘗試獲取其他連接。
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));

        }
    }

private:
    int listensock_;
    uint16_t port;
};

服務(wù)端接收連接測試

現(xiàn)在,為了驗證我們的服務(wù)器是否能夠正常接收連接請求,我們將進行一個簡單的測試。在啟動服務(wù)器程序時,我們需要指定一個端口號作為服務(wù)器的監(jiān)聽端口。接著,我們將使用這個端口號來創(chuàng)建一個服務(wù)器對象,并對該對象進行初始化操作。完成初始化后,我們就可以啟動服務(wù)器,使其開始監(jiān)聽并等待客戶端的連接請求了。

void Usage(const string& proc)
{
    cout << "Usage: " << proc << " port" << endl;
}

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit((UsageError));
    }

    uint16_t port = stoi(argv[1]);
    // TcpServer* svr = new TcpServer(port);
    unique_ptr<TcpServer> tcp_svr(new TcpServer(port));

    return 0;
}

編譯代碼后,以./tcp_server 端口號的方式運行服務(wù)端。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

運行服務(wù)端程序后,我們可以通過netstat命令查看系統(tǒng)中的網(wǎng)絡(luò)連接狀態(tài)。其中,可以看到一個名為tcp_server的服務(wù)程序正在運行,并綁定在端口8081上。由于服務(wù)器綁定了INADDR_ANY,表示它可以監(jiān)聽本地任何一張網(wǎng)卡上的數(shù)據(jù)。因此,服務(wù)器的本地IP地址顯示為0.0.0.0,這意味著該TCP服務(wù)器可以從本地任何網(wǎng)卡中接收數(shù)據(jù)。

更重要的是,當(dāng)前服務(wù)器處于LISTEN狀態(tài),這意味著它已準(zhǔn)備好接收來自外部的連接請求。這表明服務(wù)器能夠與外部客戶端建立通信,并處理傳入的請求。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

盡管我們尚未編寫客戶端的代碼,但我們?nèi)匀豢梢允褂?code>telnet命令遠程連接到該服務(wù)器。這是因為telnet底層實際上使用的是TCP協(xié)議。

通過telnet命令連接到當(dāng)前的TCP服務(wù)器后,我們可以觀察到服務(wù)器成功接收到了一個連接。為該連接提供服務(wù)的套接字對應(yīng)的文件描述符是4。這是因為在初始化服務(wù)器時,文件描述符0、1、2分別對應(yīng)于標(biāo)準(zhǔn)輸入流、標(biāo)準(zhǔn)輸出流和標(biāo)準(zhǔn)錯誤流。而文件描述符3在初始化時分配給了監(jiān)聽套接字。因此,當(dāng)?shù)谝粋€客戶端發(fā)起連接請求時,為其服務(wù)的套接字的文件描述符就是4。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

如果此時我們再用其他窗口繼續(xù)使用telnet命令,向該TCP服務(wù)器發(fā)起請求連接,此時為該客戶端提供服務(wù)的套接字對應(yīng)的文件描述符就是5。

我們直接用瀏覽器來訪問這個TCP服務(wù)器,因為瀏覽器常見的應(yīng)用層協(xié)議是http或https,其底層對應(yīng)的也是TCP協(xié)議,因此瀏覽器也可以向當(dāng)前這個TCP服務(wù)器發(fā)起請求連接。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

服務(wù)端處理請求

在TCP服務(wù)器中,一旦監(jiān)聽套接字(listening socket)成功獲取到連接請求,它不會直接為客戶端提供服務(wù)。相反,它會使用accept函數(shù)返回一個新的套接字,這個套接字專門用于與客戶端進行通信。我們將這個套接字稱為“服務(wù)套接字”。

為了確保通信的有效性,我們實現(xiàn)了一個簡單的回聲TCP服務(wù)器。在這個服務(wù)器中,當(dāng)客戶端發(fā)送數(shù)據(jù)時,服務(wù)端不會做任何復(fù)雜的處理,而是簡單地將收到的數(shù)據(jù)原封不動地返回給客戶端。這樣,客戶端在收到服務(wù)端的響應(yīng)后,可以將這些數(shù)據(jù)進行打印輸出,從而驗證服務(wù)端和客戶端之間的通信是否正常。

TCP服務(wù)器讀取數(shù)據(jù)通過read函數(shù)讀取,該函數(shù)的函數(shù)原型如下:

ssize_t read(int fd, void *buf, size_t count);

參數(shù)說明:

  • fd:特定的文件描述符,表示從該文件描述符中讀取數(shù)據(jù)。
  • buf:數(shù)據(jù)的存儲位置,表示將讀取到的數(shù)據(jù)存儲到該位置。
  • count:數(shù)據(jù)的個數(shù),表示從該文件描述符中讀取數(shù)據(jù)的字節(jié)數(shù)。

返回值說明:

  • 如果返回值大于0,則表示本次實際讀取到的字節(jié)個數(shù)。
  • 如果返回值等于0,則表示對端已經(jīng)把連接關(guān)閉了。
  • 如果返回值小于0,則表示讀取時遇到了錯誤。

read返回值為0表示對端連接關(guān)閉

這實際和本地進程間通信中的管道通信是類似的,當(dāng)使用管道進行通信時,可能會出現(xiàn)如下情況:

  • 寫端進程不寫,讀端進程一直讀,此時讀端進程就會被掛起,因為此時數(shù)據(jù)沒有就緒。
  • 讀端進程不讀,寫端進程一直寫,此時當(dāng)管道被寫滿后寫端進程就會被掛起,因為此時空間沒有就緒。
  • 寫端進程將數(shù)據(jù)寫完后將寫端關(guān)閉,此時當(dāng)讀端進程將管道當(dāng)中的數(shù)據(jù)讀完后就會讀到0
  • 讀端進程將讀端關(guān)閉,此時寫端進程就會被操作系統(tǒng)殺掉,因為此時寫端進程寫入的數(shù)據(jù)不會被讀取。

這與TCP連接中的情況類似??蛻舳岁P(guān)閉連接后,服務(wù)端讀取該連接的數(shù)據(jù)時會讀取到0。這意味著服務(wù)端已經(jīng)讀取到了客戶端發(fā)送的所有數(shù)據(jù),并且客戶端已經(jīng)關(guān)閉了連接。因此,當(dāng)服務(wù)端調(diào)用read函數(shù)并得到返回值為0時,它應(yīng)該意識到客戶端已經(jīng)關(guān)閉了連接,并停止為該客戶端提供服務(wù)。

write函數(shù)?

TCP服務(wù)器寫入數(shù)據(jù)的函數(shù)叫做write,該函數(shù)的函數(shù)原型如下:

ssize_t write(int fd, const void *buf, size_t count);

參數(shù)說明:

  • fd:特定的文件描述符,表示將數(shù)據(jù)寫入該文件描述符對應(yīng)的套接字。
  • buf:需要寫入的數(shù)據(jù)。
  • count:需要寫入數(shù)據(jù)的字節(jié)個數(shù)。

返回值說明:

  • 寫入成功返回實際寫入的字節(jié)數(shù),寫入失敗返回-1,同時錯誤碼會被設(shè)置。

當(dāng)服務(wù)端調(diào)用read函數(shù)收到客戶端的數(shù)據(jù)后,就可以再調(diào)用write函數(shù)將該數(shù)據(jù)再響應(yīng)給客戶端。

服務(wù)端處理請求

在TCP通信中,服務(wù)端通過服務(wù)套接字與客戶端進行數(shù)據(jù)的讀取和寫入。這意味著服務(wù)套接字既承擔(dān)著接收客戶端數(shù)據(jù)的角色,也負責(zé)向客戶端發(fā)送數(shù)據(jù)。這種設(shè)計正是TCP全雙工通信的體現(xiàn)。

當(dāng)服務(wù)端從服務(wù)套接字中讀取客戶端發(fā)送的數(shù)據(jù)時,如果read函數(shù)返回值為0,或者發(fā)生讀取錯誤,此時應(yīng)立即關(guān)閉服務(wù)套接字對應(yīng)的文件描述符。這是因為文件描述符是一種有限的資源,如果我們不及時釋放,會導(dǎo)致可用的文件描述符逐漸減少。因此,服務(wù)端在完成對客戶端的服務(wù)后,應(yīng)當(dāng)及時關(guān)閉對應(yīng)的文件描述符,以防止資源泄漏。

class TcpServer
{
public:
    void Start()
    {
        lg(Info,"tcpServer is running...");
        for(;;)
        {
            //1.獲取新連接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if(sockfd < 0)
            {
                lg(Warning,"accept error, errno: %d ,errstring: %s", errno, strerror(errno));
                continue;//TCP服務(wù)器不會因為某個連接獲取失敗而終止。因此,當(dāng)服務(wù)端獲取連接失敗時,應(yīng)該繼續(xù)嘗試獲取其他連接。
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));
            lg(Info, "get a new link, listensock_: %d ,%s, %d", sockfd, clientip, clientport);

            //2.根據(jù)新鏈接來進行通信
            Service(sockfd,clientip,clientport);
            close(sockfd);
        }
    }

    void Service(int sockfd,const string& clientip,const uint16_t& clientport)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd,buffer,sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;
                cout << "client say#" << buffer << endl;
                string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                write(sockfd,echo_string.c_str(),sizeof(echo_string));
            }
            else if(n == 0)
            {
                lg(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                lg(Warning,"read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }


private:
    int listensock_;
    uint16_t port_;
    string ip_;
};

客戶端創(chuàng)建套接字

創(chuàng)建客戶端對象后,我們需要對其進行初始化,其中最關(guān)鍵的步驟是創(chuàng)建套接字。與服務(wù)器端類似,客戶端在調(diào)用socket函數(shù)時也需要設(shè)置相應(yīng)的參數(shù)。

與服務(wù)器端不同,客戶端不需要進行綁定和監(jiān)聽操作:

  1. 綁定:服務(wù)器需要綁定到一個特定的IP地址和端口號,以便客戶端能夠找到并連接到它。而客戶端不需要進行綁定,因為當(dāng)它嘗試連接到服務(wù)器時,系統(tǒng)會自動為其分配一個臨時的端口號。
  2. 監(jiān)聽:服務(wù)器需要通過監(jiān)聽來等待客戶端的連接請求。而客戶端沒有需要監(jiān)聽的請求,因為它是主動發(fā)起連接的一方。

為了能夠與指定的服務(wù)器進行通信,客戶端除了自己的套接字外,還需要知道服務(wù)器的IP地址和端口號。這樣,客戶端才能通過套接字與指定的服務(wù)器建立連接并進行通信。

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

using namespace std;

void Usage(const string& proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n" <<endl;
}

// ./tcpclient serverip serverport
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    //創(chuàng)建套接字
    int sockfd = 0;
    sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        cerr << "socket error" << endl;
        return 1;
    }

    return 0;
}

客戶端連接服務(wù)器

由于客戶端不需要綁定,也不需要監(jiān)聽,因此當(dāng)客戶端創(chuàng)建完套接字后就可以向服務(wù)端發(fā)起連接請求。?

connect函數(shù)

客戶端需要調(diào)用connect()連接服務(wù)器,該函數(shù)的函數(shù)原型如下:?

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器參數(shù)說明:

  • sockfd:特定的套接字,表示通過該套接字發(fā)起連接請求。
  • addr:對端網(wǎng)絡(luò)相關(guān)的屬性信息,包括協(xié)議家族、IP地址、端口號等。
  • addrlen:傳入的addr結(jié)構(gòu)體的長度。

返回值說明:

  • 連接或綁定成功返回0,連接失敗返回-1,同時錯誤碼會被設(shè)置。

connect和bind的參數(shù)形式一致,區(qū)別在于bind的參數(shù)是自己的地址,而connect的參數(shù)是對方的地址.

客戶端連接服務(wù)器

在客戶端與服務(wù)器之間的通信中,客戶端不是不需要進行綁定,而是不需要我們自己進行綁定操作。當(dāng)客戶端向服務(wù)器發(fā)起連接請求時,系統(tǒng)會為其隨機分配一個端口號,并完成綁定操作。這樣做的目的是為了確保通信雙方能夠唯一地標(biāo)識,因為IP地址和端口號的組合是用來識別網(wǎng)絡(luò)上的不同進程或服務(wù)的。也就是說,如果connect函數(shù)調(diào)用成功了,客戶端本地會隨機給該客戶端綁定一個端口號發(fā)送給對端服務(wù)器。

此外,當(dāng)客戶端調(diào)用connect函數(shù)向服務(wù)器發(fā)起連接請求時,需要提供服務(wù)器的網(wǎng)絡(luò)信息,包括服務(wù)器的IP地址和端口號。這些信息是必要的,因為connect函數(shù)需要知道客戶端想要連接到哪個服務(wù)器。

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

using namespace std;

void Usage(const string& proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n" <<endl;
}

// ./tcpclient serverip serverport
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    struct sockaddr_in server;
    bzero(&server,sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = ntohs(serverport);
    // server.sin_addr.s_addr = inet_addr(serverip.c_str());
    inet_pton(AF_INET,serverip.c_str(),&(server.sin_addr));//換個接口使用

    int cnt = 5;
    int isreconnect = false;
    do
    {
        // tcp客戶端要不要bind?1 要不要顯示的bind?0 系統(tǒng)進行bind,隨機端口
        // 客戶端發(fā)起connect的時候,進行自動隨機bind
        int n = connect(sockfd,(struct sockaddr *)&server,sizeof(server));
        if (n < 0)
        {
            isreconnect = true;
            cnt--;
            cerr << "connect error..., reconnect: " << cnt << endl;
            sleep(2);
        }
        else
        {
            break;
        }
        
    } while (cnt && isreconnect);
    
    return 0;
}

客戶端發(fā)起請求

由于我們實現(xiàn)的是一個簡單的回聲服務(wù)器,當(dāng)客戶端成功連接到服務(wù)端后,客戶端可以開始向服務(wù)端發(fā)送數(shù)據(jù)。為了實現(xiàn)這一功能,客戶端可以使用write函數(shù)將用戶輸入的數(shù)據(jù)寫入到套接字中。

在客戶端發(fā)送數(shù)據(jù)后,服務(wù)端會讀取數(shù)據(jù)并回顯給客戶端。為了獲取服務(wù)端的響應(yīng)數(shù)據(jù),客戶端需要調(diào)用read函數(shù)來讀取從服務(wù)套接字中返回的數(shù)據(jù)。讀取到的響應(yīng)數(shù)據(jù)會被打印出來,以驗證雙方之間的通信是否正常進行。

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

using namespace std;

void Usage(const string& proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n" <<endl;
}

// ./tcpclient serverip serverport
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    struct sockaddr_in server;
    bzero(&server,sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = ntohs(serverport);
    // server.sin_addr.s_addr = inet_addr(serverip.c_str());
    inet_pton(AF_INET,serverip.c_str(),&(server.sin_addr));//換個接口使用

    //創(chuàng)建套接字
    int sockfd = 0;
    sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        cerr << "socket error" << endl;
        return 1;
    }

    //客戶端鏈接服務(wù)器
    int cnt = 5;
    int isreconnect = false;
    do
    {
        // tcp客戶端要不要bind?1 要不要顯示的bind?0 系統(tǒng)進行bind,隨機端口
        // 客戶端發(fā)起connect的時候,進行自動隨機bind
        int n = connect(sockfd,(struct sockaddr *)&server,sizeof(server));
        if (n < 0)
        {
            isreconnect = true;
            cnt--;
            cerr << "connect error..., reconnect: " << cnt << endl;
            sleep(2);
        }
        else
        {
            break;
        }
        
    } while (cnt && isreconnect);
    
    //客戶端發(fā)起請求
    while(true)
    {
        string message;
        cout << "Please Enter@:" << endl;
        getline(cin,message);

        int n = write(sockfd,message.c_str(),message.size());
        if(n < 0)
        {
            std::cerr << "write error..." << std::endl;
        }

        char inbuffer[4096];

        n = read(sockfd,inbuffer,sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        else if(n == 0)
        {
            cout << "server close!" << endl;
            break;  
        }
        else
        {
            cerr << "read error!" << endl;
            break;
        }
    }

    return 0;
}

服務(wù)器測試

在完成服務(wù)端和客戶端的編寫后,下一步是進行測試。首先啟動服務(wù)端,然后使用netstat命令檢查網(wǎng)絡(luò)狀態(tài)。通過netstat命令,我們可以看到名為tcp_server的服務(wù)進程正在監(jiān)聽狀態(tài)等待客戶端的連接請求。這意味著服務(wù)端已經(jīng)準(zhǔn)備好接收客戶端的連接請求,并開始等待客戶端的連接。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

然后再通過./tcp_client IP地址 端口號的形式運行客戶端,此時客戶端就會向服務(wù)端發(fā)起連接請求,服務(wù)端獲取到請求后就會為該客戶端提供服務(wù)。

當(dāng)客戶端向服務(wù)端發(fā)送消息后,服務(wù)端可以通過打印的IP地址和端口號識別出對應(yīng)的客戶端,而客戶端也可以通過服務(wù)端響應(yīng)回來的消息來判斷服務(wù)端是否收到了自己發(fā)送的消息。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

如果此時客戶端退出了,那么服務(wù)端在調(diào)用read函數(shù)時得到的返回值就是0,此時服務(wù)端也就知道客戶端退出了,進而會終止對該客戶端的服務(wù)。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

注意: 此時是服務(wù)端對該客戶端的服務(wù)終止了,而不是服務(wù)器終止了,此時服務(wù)器依舊在運行,它在等待下一個客戶端的連接請求。?

單執(zhí)行流服務(wù)器的弊端

以上我們實現(xiàn)的是單執(zhí)行流的服務(wù)器。當(dāng)我們僅用一個客戶端連接服務(wù)端時,這一個客戶端能夠正常使用到服務(wù)端的服務(wù)。?

當(dāng)一個客戶端正在與服務(wù)器進行通信時,另一個客戶端嘗試連接到服務(wù)器。雖然顯示連接成功,但第二個客戶端發(fā)送給服務(wù)端的數(shù)據(jù)并沒有在服務(wù)端被打印出來,并且服務(wù)端也沒有將該數(shù)據(jù)回顯給該客戶端。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

只有當(dāng)?shù)谝粋€客戶端退出后,服務(wù)端才會將第二個客戶端發(fā)來是數(shù)據(jù)進行打印,并回顯該第二個客戶端。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

通過實驗觀察,我們發(fā)現(xiàn)服務(wù)端在處理一個客戶端請求后,才會繼續(xù)處理下一個客戶端的請求。這是因為當(dāng)前的服務(wù)端實現(xiàn)是基于單執(zhí)行流的模型,一次只能為一個客戶端提供服務(wù)。

當(dāng)服務(wù)端調(diào)用accept函數(shù)接受一個客戶端連接后,就開始為該客戶端提供服務(wù)。然而,在服務(wù)端為某個客戶端提供服務(wù)的過程中,其他客戶端可能會發(fā)起連接請求。但由于單執(zhí)行流的限制,服務(wù)端必須先完成當(dāng)前客戶端的服務(wù),然后才能處理下一個客戶端的請求。

為什么第一個客戶端退出后第二個客戶端會回顯成功?

  • 當(dāng)服務(wù)端正在為第一個客戶端提供服務(wù)時,第二個客戶端成功發(fā)起了連接請求。然而,服務(wù)端并沒有通過調(diào)用accept函數(shù)來接收這個新的連接。實際上,在底層系統(tǒng)會為這種情況維護一個連接隊列。沒有通過accept函數(shù)獲取的新連接會被放入這個連接隊列中。這個連接隊列的最大長度可以通過listen函數(shù)的第二個參數(shù)來指定。
  • 因此,雖然服務(wù)端沒有直接處理第二個客戶端的連接請求,但在第二個客戶端看來,連接是成功的。這是因為系統(tǒng)將連接請求放入了連接隊列中,等待服務(wù)端空閑時再處理。

那么我們?nèi)绾谓鉀Q這種情況呢??

  • 單執(zhí)行流的服務(wù)器在處理客戶端請求時,一次只能為一個客戶端提供服務(wù)。這種方式的缺點是服務(wù)器的資源無法得到充分利用。為了提高服務(wù)器的效率和并發(fā)處理能力,通常會將服務(wù)器改為多執(zhí)行流的設(shè)計。
  • 要實現(xiàn)多執(zhí)行流,需要引入多進程或多線程技術(shù)。通過創(chuàng)建多個進程或線程,服務(wù)器可以同時處理多個客戶端的請求,從而更好地利用服務(wù)器的資源。這種設(shè)計可以大大提高服務(wù)器的處理能力和并發(fā)性能,滿足更多客戶端的需求。
  • 因此,將服務(wù)器從單執(zhí)行流改為多執(zhí)行流是多進程或多線程技術(shù)應(yīng)用的一個重要方面,它可以顯著提升服務(wù)器的性能和并發(fā)處理能力。

多進程的TCP網(wǎng)絡(luò)程序

當(dāng)服務(wù)端通過accept函數(shù)獲取到一個新的客戶端連接時,它并不會由當(dāng)前的執(zhí)行流程(通常是主進程)直接為這個連接提供服務(wù)。相反,主進程會調(diào)用fork函數(shù)來創(chuàng)建一個新的子進程。然后,子進程將負責(zé)為這個新建立的連接提供服務(wù)。

由于父進程(主進程)和子進程是彼此獨立的執(zhí)行流,當(dāng)父進程創(chuàng)建了子進程之后,它可以繼續(xù)回到監(jiān)聽套接字上等待并接受新的連接請求,而無需等待或關(guān)注子進程何時完成對當(dāng)前連接的服務(wù)。這種方式允許服務(wù)器同時處理多個客戶端連接,提高了服務(wù)器的并發(fā)處理能力。

子進程繼承父進程的文件描述符表

需要注意的是,文件描述符表是進程特有的資源,子進程在創(chuàng)建時會繼承父進程的文件描述符表。這意味著,如果父進程打開了一個文件并獲得了一個文件描述符(例如3號文件描述符),那么子進程也會擁有這個文件描述符,并指向同一個打開的文件。

進一步地,如果子進程再創(chuàng)建子進程,那么孫子進程同樣會繼承這個文件描述符,并指向相同的文件。這種繼承關(guān)系使得多個進程可以共享同一個文件描述符,從而實現(xiàn)對同一文件的訪問。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

在進程創(chuàng)建子進程后,父子進程之間保持相對獨立性。這意味著父進程的文件描述符表的變化不會對子進程產(chǎn)生影響。這種獨立性在匿名管道的通信中表現(xiàn)得尤為明顯。

父進程通過pipe函數(shù)創(chuàng)建了一個匿名管道,并獲取了兩個文件描述符,一個用于讀取管道數(shù)據(jù),另一個用于寫入數(shù)據(jù)。子進程在創(chuàng)建時會繼承這兩個文件描述符。之后,父進程和子進程會分別關(guān)閉管道的讀端和寫端,這樣做是為了確保單向通信的順利進行。

在這種設(shè)置下,父子進程通過這個管道進行通信,而文件描述符表的變化不會相互干擾。同樣地,套接字文件也是一樣,子進程繼承了父進程的套接字文件描述符,從而能夠讀寫特定的套接字文件,實現(xiàn)對客戶端的服務(wù)。

等待子進程問題

在父進程創(chuàng)建子進程后,為了防止子進程變成僵尸進程并導(dǎo)致內(nèi)存泄漏,父進程需要等待子進程的退出。否則,子進程將無法釋放其資源,導(dǎo)致系統(tǒng)資源的浪費。因此,服務(wù)端在創(chuàng)建子進程后需要調(diào)用wait或waitpid函數(shù)來等待子進程的完成。

關(guān)于阻塞式等待與非阻塞式等待:

  • 阻塞式等待:如果服務(wù)端采用阻塞方式等待子進程,那么在為當(dāng)前客戶端提供服務(wù)期間,服務(wù)端將無法繼續(xù)處理其他連接請求。這意味著服務(wù)端仍然是以串行方式處理客戶端請求,無法充分利用多執(zhí)行流的優(yōu)點。
  • 非阻塞式等待:雖然非阻塞方式允許服務(wù)端在子進程為客戶端提供服務(wù)時繼續(xù)接收新連接,但這種方式需要服務(wù)端保存所有子進程的PID,并不斷檢測子進程是否退出。這增加了服務(wù)端的開銷和復(fù)雜性。

總體而言,無論是阻塞式還是非阻塞式等待子進程的方式都有其不足之處。為了更好地利用多執(zhí)行流的優(yōu)勢,可以考慮讓服務(wù)端不等待子進程的退出。這樣可以進一步提高服務(wù)器的并發(fā)處理能力和效率。

要讓父進程不等待子進程的退出,有幾種常見的方法:

  1. 多級創(chuàng)建子進程:父進程可以創(chuàng)建子進程,然后讓子進程再創(chuàng)建孫子進程。最后,孫子進程負責(zé)為客戶端提供服務(wù)。這樣,父進程和子進程可以繼續(xù)處理其他任務(wù),而父進程只需要關(guān)系兒子進程,因此不需要等待孫子進程的退出,最后子進程會由1號進程進行回收。
  2. 捕捉SIGCHLD信號:父進程可以捕捉SIGCHLD信號,并將該信號的處理動作設(shè)置為忽略。當(dāng)子進程退出時,父進程會收到SIGCHLD信號,但由于處理動作被設(shè)置為忽略,父進程可以繼續(xù)執(zhí)行其他任務(wù),而不需要等待子進程的退出。

通過這些方法,父進程可以更加靈活地處理子進程的退出,并更好地利用多執(zhí)行流的優(yōu)勢,提高服務(wù)器的并發(fā)處理能力和效率。

多級創(chuàng)建子進程

我讓服務(wù)端創(chuàng)建出來的子進程再次進行fork,讓孫子進程為客戶端提供服務(wù), 此時我們就不用等待孫子進程退出了。?

  • 父進程:在服務(wù)端調(diào)用accept函數(shù)獲取客戶端連接請求的進程。
  • 子進程:由父進程調(diào)用fork函數(shù)創(chuàng)建出來的進程。
  • 孫子進程:由子進程調(diào)用fork函數(shù)創(chuàng)建出來的進程,該進程調(diào)用Service函數(shù)為客戶端提供服務(wù)。

我們讓子進程創(chuàng)建完孫子進程后立刻退出,此時服務(wù)進程(父進程)調(diào)用wait/waitpid函數(shù)等待子進程就能立刻等待成功,此后服務(wù)進程就能繼續(xù)調(diào)用accept函數(shù)獲取其他客戶端的連接請求。

為什么不需要等待孫子進程退出?

由于子進程在創(chuàng)建孫子進程后立即退出,孫子進程會變成孤兒進程。在這種情況下,系統(tǒng)會接管對孫子進程的管理。當(dāng)孫子進程為客戶端提供完服務(wù)并退出時,系統(tǒng)會回收相關(guān)資源。此外,每個父進程只需關(guān)注自己的子進程,不需要等待孫子進程的退出。因此,服務(wù)進程(父進程)可以更加高效地處理客戶端請求,而不需要等待子進程或?qū)O子進程的退出。?

關(guān)閉對應(yīng)的文件描述符

當(dāng)服務(wù)進程(父進程)調(diào)用accept函數(shù)獲取到新連接時,它會通過子進程來創(chuàng)建孫子進程,并由孫子進程為該連接提供服務(wù)。在此過程中,服務(wù)進程、子進程和孫子進程各自的文件描述符表是獨立的,不會相互影響。

由于服務(wù)進程在調(diào)用fork函數(shù)后創(chuàng)建了子進程,它不再需要關(guān)心從accept函數(shù)獲取的文件描述符。因此,服務(wù)進程可以調(diào)用close函數(shù)關(guān)閉該文件描述符,釋放相關(guān)資源。

同樣地,子進程和孫子進程也不需要關(guān)心從服務(wù)進程繼承的監(jiān)聽套接字。因此,子進程可以選擇關(guān)閉這個監(jiān)聽套接字,以釋放資源并確保系統(tǒng)的正常運行。

這種設(shè)計方式充分利用了多執(zhí)行流的優(yōu)勢,使得服務(wù)進程、子進程和孫子進程可以并行處理不同的任務(wù),提高服務(wù)器的并發(fā)處理能力和效率。同時,獨立的文件描述符表和資源管理確保了系統(tǒng)的穩(wěn)定性和可靠性。

關(guān)閉文件描述符的必要性:

  • 對于服務(wù)進程來說,當(dāng)它調(diào)用fork函數(shù)后,必須關(guān)閉從accept函數(shù)獲取的文件描述符。這是因為服務(wù)進程會持續(xù)地調(diào)用accept函數(shù)來獲取新的文件描述符(服務(wù)套接字)。如果服務(wù)進程不及時關(guān)閉不再使用的文件描述符,會導(dǎo)致可用文件描述符逐漸減少,從而影響到服務(wù)器的正常運作。
  • 對于子進程和孫子進程來說,建議關(guān)閉從服務(wù)進程繼承下來的監(jiān)聽套接字。實際上,即使它們不關(guān)閉監(jiān)聽套接字,只會造成該文件描述符的泄漏,但通常還是建議關(guān)閉。因為孫子進程在提供服務(wù)時可能會對監(jiān)聽套接字進行誤操作,這可能會對監(jiān)聽套接字中的數(shù)據(jù)造成影響。

因此,為了確保服務(wù)器的穩(wěn)定性和可靠性,及時關(guān)閉不再使用的文件描述符是必要的。同時,關(guān)閉繼承自服務(wù)進程的監(jiān)聽套接字也是為了避免潛在的誤操作和數(shù)據(jù)影響。

class TcpServer
{
public:
    TcpServer(const uint16_t& serverport,const string& ip = defaultip)
        :listensock_(defaultfd),
        port_(serverport),
        ip_(ip)
    {}

    void Start()
    {
        lg(Info,"tcpServer is running...");
        for(;;)
        {
            //1.獲取新連接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if(sockfd < 0)
            {
                lg(Warning,"accept error, errno: %d ,errstring: %s", errno, strerror(errno));
                continue;//TCP服務(wù)器不會因為某個連接獲取失敗而終止。因此,當(dāng)服務(wù)端獲取連接失敗時,應(yīng)該繼續(xù)嘗試獲取其他連接。
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));
            lg(Info, "get a new link, listensock_: %d ,%s, %d", sockfd, clientip, clientport);

            //2.根據(jù)新鏈接來進行通信
            // version 1 -- 單進程版
            // Service(sockfd,clientip,clientport);
            // close(sockfd);
            // version 2 -- 多進程版
            pid_t id = fork();//創(chuàng)建子進程
            if(id == 0)//子進程
            {
                //child
                close(listensock_);
                if(fork() > 0) exit(0);//創(chuàng)建孫子進程,子進程直接退出,父進程就不用阻塞等待
                Service(sockfd,clientip,clientport);//孫子進程進行服務(wù), system 領(lǐng)養(yǎng)
                close(sockfd);
                exit(0);//孫子進程提供完服務(wù)退出
            }
            //父進程
            close(sockfd);//father關(guān)閉為連接提供服務(wù)的套接字
            pid_t rid = waitpid(id,nullptr,0);//等待子進程(會立刻等待成功)
        }
    }

    ~TcpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }

private:
    int listensock_;//監(jiān)聽套接字
    uint16_t port_;//端口號
    string ip_;
};

服務(wù)器測試

重新編譯程序運行客戶端后,繼續(xù)使用監(jiān)控腳本對服務(wù)進程進行實時監(jiān)控。

while :; do ps axj | head -1 && ps axj | grep tcpserver | grep -v grep;echo "######################";sleep 1;done

我們看到一開始沒有客戶端連接服務(wù)器,因此也是只監(jiān)控到了一個服務(wù)進程,該服務(wù)進程正在等待客戶端的請求連接。

然后我們運行一個客戶端,讓該客戶端連接當(dāng)前這個服務(wù)器,此時服務(wù)進程會創(chuàng)建出子進程,子進程再創(chuàng)建出孫子進程,之后子進程就會立刻退出,而由孫子進程為客戶端提供服務(wù)。因此這時我們只看到了兩個服務(wù)進程,其中一個是一開始用于獲取連接的服務(wù)進程,還有一個就是孫子進程,該進程為當(dāng)前客戶端提供服務(wù),它的PPID為1,表明這是一個孤兒進程。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

當(dāng)我們運行第二個客戶端連接服務(wù)器時,此時就又會創(chuàng)建出一個孤兒進程為該客戶端提供服務(wù)。我們看到兩個進程的文件描述符都是4。因為父進程將子進程的文件描述符關(guān)閉了。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

此時,這兩個客戶端是由兩個獨立的孤兒進程提供服務(wù)的。由于它們運行在不同的進程中,因此可以同時接收服務(wù),不會互相干擾??梢钥吹剑蛻舳税l(fā)送給服務(wù)端的數(shù)據(jù)都能在服務(wù)端得到輸出,并且服務(wù)端會根據(jù)這些數(shù)據(jù)進行相應(yīng)的響應(yīng)。這種設(shè)計方式充分利用了多進程的并發(fā)處理能力,提高了服務(wù)器的效率和性能,確保了多個客戶端能夠同時享受到服務(wù)。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器?當(dāng)客戶端全部退出后,對應(yīng)為客戶端提供服務(wù)的孤兒進程也會跟著退出,這時這些孤兒進程會被系統(tǒng)回收,而最終剩下那個獲取連接的服務(wù)進程。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

捕捉SIGCHLD信號

在實際操作中,當(dāng)子進程退出時,它會向父進程發(fā)送SIGCHLD信號。如果父進程捕捉到這個信號,并將其處理動作設(shè)置為忽略,那么父進程就可以專注于自己的工作,而不必過分關(guān)注子進程的狀態(tài)。

該方式實現(xiàn)起來非常簡單,也是比較推薦的一種做法。

class TcpServer
{
public:
    TcpServer(const uint16_t& serverport,const string& ip = defaultip)
        :listensock_(defaultfd),
        port_(serverport),
        ip_(ip)
    {}

    void Start()
    {
        signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信號
        lg(Info,"tcpServer is running...");
        for(;;)
        {
            //1.獲取新連接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if(sockfd < 0)
            {
                lg(Warning,"accept error, errno: %d ,errstring: %s", errno, strerror(errno));
                continue;//TCP服務(wù)器不會因為某個連接獲取失敗而終止。因此,當(dāng)服務(wù)端獲取連接失敗時,應(yīng)該繼續(xù)嘗試獲取其他連接。
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(client));
            lg(Info, "get a new link, listensock_: %d ,%s, %d", sockfd, clientip, clientport);

            //2.根據(jù)新鏈接來進行通信
            // version 2 -- 多進程版
            //捕捉SIGCHLD信號版
            pid_t id = fork();//創(chuàng)建子進程
            if(id == 0)//子進程
            {
                close(listensock_);
                Service(sockfd,clientip,clientport);
                exit(0);
            }
        }
    }

    ~TcpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }

private:
    int listensock_;//監(jiān)聽套接字
    uint16_t port_;//端口號
    string ip_;
};

代碼測試

重新編譯程序運行服務(wù)端后,可以通過以下監(jiān)控腳本對服務(wù)進程進行監(jiān)控。

while :; do ps axj | head -1 && ps axj | grep tcpserver | grep -v grep;echo "######################";sleep 1;done

當(dāng)客戶端連接到服務(wù)器時,服務(wù)進程會通過調(diào)用fork函數(shù)來創(chuàng)建一個子進程,該子進程專門負責(zé)為該客戶端提供服務(wù)。這種設(shè)計充分利用了多執(zhí)行流的優(yōu)勢,使得多個客戶端可以同時獲得服務(wù),而不會相互干擾。

如果又有新的客戶端連接至服務(wù)器,服務(wù)進程會再次調(diào)用fork函數(shù)創(chuàng)建另一個子進程,以確保新客戶端也能獲得獨立的服務(wù)。這兩個客戶端由不同的子進程提供服務(wù),這意味著它們可以同時享受服務(wù),并且發(fā)送給服務(wù)端的數(shù)據(jù)都能得到輸出和響應(yīng)。

測試結(jié)果如下:

此時我們運行一個客戶端,讓該客戶端連接服務(wù)器,此時服務(wù)進程就會調(diào)用fork函數(shù)創(chuàng)建出一個子進程,由該子進程為這個客戶端提供服務(wù)。

如果再有一個客戶端連接服務(wù)器,此時服務(wù)進程會再創(chuàng)建出一個子進程,讓該子進程為這個客戶端提供服務(wù)。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

當(dāng)客戶端一個個退出后,在服務(wù)端對應(yīng)為之提供服務(wù)的子進程也會相繼退出,但無論如何服務(wù)端都至少會有一個服務(wù)進程,這個服務(wù)進程的任務(wù)就是不斷獲取新連接。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

多線程的TCP網(wǎng)絡(luò)程序

創(chuàng)建進程涉及到多個重要的數(shù)據(jù)結(jié)構(gòu),如進程控制塊(task_struct)、進程地址空間(mm_struct)和頁表等,這些結(jié)構(gòu)的創(chuàng)建和維護都需要一定的資源和時間。相比之下,創(chuàng)建線程的成本較低,因為線程是在進程地址空間內(nèi)運行的,可以共享進程的大部分資源。

在實現(xiàn)多執(zhí)行流的服務(wù)器時,使用多線程技術(shù)是一個更好的選擇。當(dāng)服務(wù)進程通過調(diào)用accept函數(shù)獲取到一個新連接時,可以直接創(chuàng)建一個線程,由該線程為客戶端提供服務(wù)。

值得注意的是,雖然主線程(服務(wù)進程)創(chuàng)建了新線程,但仍然需要等待新線程退出,否則可能會導(dǎo)致類似于僵尸進程的問題。然而,對于線程來說,如果不想讓主線程等待新線程退出,可以選擇調(diào)用pthread_detach函數(shù)來實現(xiàn)線程分離。這樣,當(dāng)該線程退出時,系統(tǒng)會自動回收其資源。

通過這種設(shè)計,主線程(服務(wù)進程)可以繼續(xù)調(diào)用accept函數(shù)來獲取新連接,同時新線程可以為對應(yīng)的客戶端提供服務(wù)。這種多線程的實現(xiàn)方式能夠更好地利用系統(tǒng)資源,提高服務(wù)器的并發(fā)處理能力和效率。

各個線程共享同一張文件描述符表?

文件描述符表是用于維護進程與文件之間對應(yīng)關(guān)系的數(shù)據(jù)結(jié)構(gòu)。每個進程都有自己獨立的文件描述符表,因此一個進程對應(yīng)一張文件描述符表。當(dāng)主線程(服務(wù)進程)創(chuàng)建新線程時,新線程仍然屬于同一個進程。這意味著在創(chuàng)建線程時,不會為新線程創(chuàng)建獨立的文件描述符表。所有的線程共享同一張文件描述符表,這意味著它們可以訪問和操作同一個文件描述符集合。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

在服務(wù)器中,當(dāng)服務(wù)進程(主線程)通過調(diào)用accept函數(shù)獲取到一個文件描述符后,新創(chuàng)建的線程可以直接訪問和使用這個文件描述符。這是因為所有線程共享同一個文件描述符表,它們能夠訪問相同的文件描述符集合。

重要的是要注意,雖然新線程可以訪問主線程通過accept函數(shù)獲取的文件描述符,但新線程并不知道它所服務(wù)的客戶端對應(yīng)的文件描述符。因此,主線程在創(chuàng)建新線程后,需要明確告訴每個新線程應(yīng)該訪問的文件描述符的值。這確保了每個新線程在為特定客戶端提供服務(wù)時,能夠準(zhǔn)確地操作正確的套接字。

參數(shù)結(jié)構(gòu)體

當(dāng)新線程為客戶端提供服務(wù)時,它需要調(diào)用Service函數(shù),并向其傳遞三個關(guān)鍵參數(shù):客戶端的套接字、IP地址和端口號。為了確保每個線程都能為正確的客戶端提供服務(wù),主線程在創(chuàng)建新線程時需要傳遞這些參數(shù)。

然而,pthread_create函數(shù)在創(chuàng)建新線程時,只能接受一個類型為void*的參數(shù)。為了解決這個問題,我們可以設(shè)計一個參數(shù)結(jié)構(gòu)體Param,將這三個參數(shù)封裝到該結(jié)構(gòu)體中。

主線程在創(chuàng)建新線程時,可以定義一個Param對象,將客戶端的套接字、IP地址和端口號初始化到該對象中。然后,將Param對象的地址作為參數(shù)傳遞給新線程的執(zhí)行例程。

在新線程的執(zhí)行例程中,可以將這個void類型的參數(shù)強制轉(zhuǎn)換為Param類型,從而獲得客戶端的套接字、IP地址和端口號。這樣,新線程就能夠正確地調(diào)用Service函數(shù),為相應(yīng)的客戶端提供服務(wù)。

class ThreadData
{
public:
    ThreadData(int fd,const string& ip,const uint16_t port)
    :sockfd(fd),
    clientip(ip),
    clientport(port)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
};

文件描述符關(guān)閉的問題?

由于所有線程共享同一張文件描述符表,因此在操作文件描述符表時,需要考慮線程間的協(xié)同工作。

  • 對于主線程通過accept函數(shù)獲取的文件描述符,主線程不應(yīng)該直接進行關(guān)閉操作。這是因為新線程負責(zé)為客戶端提供服務(wù),因此只有在新線程完成服務(wù)后,才應(yīng)該關(guān)閉該文件描述符。這樣做可以確保資源得到正確的釋放,并避免因線程間的競態(tài)條件而導(dǎo)致的錯誤操作。
  • 另外,雖然新線程不需要關(guān)注監(jiān)聽套接字,但同樣不能關(guān)閉監(jiān)聽套接字對應(yīng)的文件描述符。因為關(guān)閉監(jiān)聽套接字會導(dǎo)致主線程無法從該套接字中接收新的連接請求。

參數(shù)結(jié)構(gòu)體增加TcpServer類的成員變量

  • 由于調(diào)用pthread_create函數(shù)創(chuàng)建線程時,新線程的執(zhí)行例程是一個參數(shù)為void*,返回值為void*的函數(shù)。如果我們要將這個執(zhí)行例程定義到類內(nèi),就需要將其定義為靜態(tài)成員函數(shù),否則這個執(zhí)行例程的第一個參數(shù)是隱藏的this指針。
  • 在線程的執(zhí)行例程當(dāng)中會調(diào)用Service函數(shù),由于執(zhí)行例程是靜態(tài)成員函數(shù),靜態(tài)成員函數(shù)無法調(diào)用非靜態(tài)成員函數(shù)。因此我們在參數(shù)結(jié)構(gòu)體中定義一個TcpServer類的變量,這樣執(zhí)行例程就可以調(diào)用TcpServer類中Service函數(shù)。(我們也可以將Service函數(shù)定義為靜態(tài)成員函數(shù)。恰好Service函數(shù)內(nèi)部進行的操作都是與類無關(guān)的,因此我們直接在Service函數(shù)前面加上一個static即可。)
class TcpServer;//ThreadData要用到,提前聲明

class ThreadData
{
public:
    ThreadData(int fd,const string& ip,const uint16_t port, TcpServer *t)
    :sockfd(fd),
    clientip(ip),
    clientport(port),
    tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

class TcpServer
{
public:
    TcpServer(const uint16_t& serverport,const string& ip = defaultip)
        :listensock_(defaultfd),
        port_(serverport),
        ip_(ip)
    {}
    //version 3 -- 多線程版本
    static void* Rountine(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData* td = static_cast<ThreadData*>(args);
        td->tsvr->Service(td->sockfd,td->clientip,td->clientport);
        delete td;
        return nullptr;
    }

    void Start()
    {
        lg(Info,"tcpServer is running...");
        for(;;)
        {
            //1.獲取新連接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if(sockfd < 0)
            {
                lg(Warning,"accept error, errno: %d ,errstring: %s", errno, strerror(errno));
                continue;//TCP服務(wù)器不會因為某個連接獲取失敗而終止。因此,當(dāng)服務(wù)端獲取連接失敗時,應(yīng)該繼續(xù)嘗試獲取其他連接。
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(clientip));
            lg(Info, "get a new link, listensock_: %d ,%s, %d", sockfd, clientip, clientport);

            //2.根據(jù)新鏈接來進行通信
            //version 3 -- 多線程版本
            ThreadData* td = new ThreadData(sockfd,clientip,clientport,this);
            pthread_t tid;
            pthread_create(&tid,nullptr,Rountine,td);

        }
    }

    void Service(int sockfd,const string& clientip,const uint16_t& clientport)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd,buffer,sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;
                cout << "client say#" << buffer << endl;
                string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                write(sockfd,echo_string.c_str(),echo_string.size());
            }
            else if(n == 0)
            {
                lg(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                lg(Warning,"read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }

    ~TcpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }

private:
    int listensock_;//監(jiān)聽套接字
    uint16_t port_;//端口號
    string ip_;
};

代碼測試

此時我們再重新編譯服務(wù)端代碼,由于代碼當(dāng)中用到了多線程,因此編譯時需要攜帶上-pthread選項。此外,由于我們現(xiàn)在要監(jiān)測的是一個個的線程,因此在監(jiān)控時使用的不再是ps -axj命令,而是ps -aL命令。

while :; do ps -aL|head -1&&ps -aL|grep tcpserver;echo "####################";sleep 1;done

運行服務(wù)端,通過監(jiān)控可以看到,此時只有一個服務(wù)線程,該服務(wù)線程就是主線程,它現(xiàn)在在等待客戶端的連接到來。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

當(dāng)客戶端連接到服務(wù)端時,主線程會為該客戶端構(gòu)建一個參數(shù)結(jié)構(gòu)體。接著,主線程會創(chuàng)建一個新線程,并將參數(shù)結(jié)構(gòu)體的地址作為參數(shù)傳遞給新線程。

通過這種方式,新線程能夠訪問參數(shù)結(jié)構(gòu)體并從中提取出所需的參數(shù)。新線程使用這些參數(shù)調(diào)用Service函數(shù),為特定的客戶端提供服務(wù)。

在監(jiān)控過程中,由于主線程和新線程同時運行,因此會顯示兩個線程。主線程負責(zé)監(jiān)聽新的連接請求,而新線程則負責(zé)處理特定客戶端的服務(wù)請求。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

當(dāng)?shù)诙€客戶端的連接請求到達服務(wù)端時,主線程會執(zhí)行與第一個客戶端連接時相同的操作。它會為第二個客戶端構(gòu)建一個新的參數(shù)結(jié)構(gòu)體,并再次創(chuàng)建一個新線程。這個新線程將使用從參數(shù)結(jié)構(gòu)體中提取的信息,為第二個客戶端提供所需的服務(wù)。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

由于每個客戶端都有各自的服務(wù)線程為其提供服務(wù),這意味著服務(wù)端為這兩個客戶端提供了獨立的執(zhí)行流。這使得兩個客戶端可以同時接收服務(wù)端的響應(yīng),并享受服務(wù)端提供的并行服務(wù)。

當(dāng)這兩個客戶端向服務(wù)端發(fā)送消息時,這些消息可以在服務(wù)端被正確地打印出來。同樣,客戶端也能收到服務(wù)端返回的回顯數(shù)據(jù)。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

無論客戶端的連接請求有多少,服務(wù)端都會動態(tài)地創(chuàng)建相應(yīng)數(shù)量的新線程。當(dāng)客戶端完成與服務(wù)端的交互并退出時,為其提供服務(wù)的新線程也會相應(yīng)地結(jié)束任務(wù)并退出。這意味著服務(wù)端的線程數(shù)量會隨著客戶端的連接和斷開而動態(tài)變化。

最終,當(dāng)所有客戶端都斷開連接后,服務(wù)端將只剩下最初的主線程在運行。主線程會持續(xù)監(jiān)聽新的連接請求,隨時準(zhǔn)備新連接的到來。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

線程池的TCP網(wǎng)絡(luò)程序?

多線程版本存在的問題:當(dāng)大量的客戶端連接到服務(wù)器時,服務(wù)器會為每個客戶端創(chuàng)建一個新的服務(wù)線程。然而,這種方法存在幾個問題:

  1. 資源消耗:每當(dāng)有新連接到來時,服務(wù)端的主線程都會重新為該客戶端創(chuàng)建為其提供服務(wù)的新線程,而當(dāng)服務(wù)結(jié)束后又會將該新線程銷毀。這樣做不僅麻煩,而且效率低下,
  2. 上下文切換開銷:當(dāng)線程數(shù)量過多時,CPU在切換線程時需要進行上下文切換,這需要花費一定的時間。頻繁的上下文切換會導(dǎo)致線程響應(yīng)變慢,影響服務(wù)器的性能。
  3. 線程管理開銷:每個線程都需要被適當(dāng)?shù)毓芾砗驼{(diào)度,這需要一定的計算和存儲開銷。當(dāng)線程數(shù)量過多時,線程管理的復(fù)雜性也會增加,這可能導(dǎo)致性能下降。
  4. 服務(wù)質(zhì)量不穩(wěn)定:一旦線程太多,每一個線程再次被調(diào)度的周期就變長了,而線程是為客戶端提供服務(wù)的,線程被調(diào)度的周期變長,客戶端也遲遲得不到應(yīng)答。

針對上述問題,我們可以采取以下解決思路:

1. 線程池技術(shù)

預(yù)先在服務(wù)器端創(chuàng)建一定數(shù)量的線程,形成一個線程池。當(dāng)有新的客戶端連接請求時,線程池中的線程可以立即為客戶端提供服務(wù),避免了為每個客戶端單獨創(chuàng)建線程的開銷和延遲。

2. 線程復(fù)用

當(dāng)一個線程完成對某個客戶端的服務(wù)后,它并不會被銷毀,而是繼續(xù)為下一個客戶端提供服務(wù)。這樣可以大大減少線程的創(chuàng)建和銷毀開銷,提高服務(wù)器的處理效率。

3. 線程休眠與喚醒

在沒有客戶端請求時,線程可以進入休眠狀態(tài),以降低CPU的負載。當(dāng)有新的客戶端請求到來時,休眠的線程可以被喚醒并立即為客戶端提供服務(wù)。

4. 連接隊列

當(dāng)所有線程都在為其他客戶端提供服務(wù),而又有新的客戶端連接請求到來時,可以將這些請求放入連接隊列中等待。一旦有線程空閑出來,就可以從隊列中取出請求并為其提供服務(wù)。

通過上述方法,可以有效解決單純多線程服務(wù)器存在的問題,提高服務(wù)器的并發(fā)處理能力和性能。

引入線程池

為了解決上述問題,我們需要在服務(wù)端引入線程池。線程池的目的是降低處理短時間任務(wù)時創(chuàng)建和銷毀線程的開銷,并確保內(nèi)核資源的充分利用,防止過度調(diào)度。

線程池的核心組件是任務(wù)隊列。當(dāng)新的任務(wù)到來時,它會被推送到任務(wù)隊列中。線程池預(yù)先創(chuàng)建了若干個線程,這些線程會不斷檢查任務(wù)隊列,看是否有待處理的任務(wù)。一旦發(fā)現(xiàn)任務(wù),線程會取出任務(wù)并調(diào)用其Run函數(shù)進行處理。如果沒有任務(wù),線程會進入休眠狀態(tài),等待新的任務(wù)到來。?

(我在前面寫過一篇線程池的博客,大家可以去看一下,這里我就講解線程池接入的方法。)

#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <pthread.h>
#include <unistd.h>

using namespace std;

struct ThreadInfo
{
    pthread_t tid;
    string name;
};

static const int defaultnum = 5;

//線程池
template<class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void UnLock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_,&mutex_);
    }
    bool IsQueueEmpty()
    {
        return task_.empty();
    }
    string GetThreadName(pthread_t tid)
    {
        int num = threads_.size();
        for(int i = 0;i < num;++i)
        {
            if(threads_[i].tid == tid)
                return threads_[i].name;
        }

        return "none";
    }
public:

    //線程池中線程的執(zhí)行例程
    static void* HandlerTask(void* args)
    {
        ThreadPool<T>* tp = static_cast<ThreadPool<T> *>(args);//?
        string name = tp->GetThreadName(pthread_self());

        //不斷從任務(wù)隊列獲取任務(wù)進行處理
        while (true)
        {
            //消費任務(wù)
            tp->Lock();

            while (tp->IsQueueEmpty())//任務(wù)隊列為空
            {
                tp->ThreadSleep();
            }

            T t = tp->Pop();
            tp->UnLock();
            
            t();//處理任務(wù)
        }
        
    }

    void Start()
    {
        int num = threads_.size();
        for(int i = 0;i < num;++i)
        {
            threads_[i].name = "thread-" + to_string(i);
            //pthread_create要求HandlerTask函數(shù)是void*返回值,參數(shù)void*類型,如果HandlerTask函數(shù)直接定義在類里面,那么參數(shù)還會有一個隱藏的this
            //指針,類型就不匹配了,編譯會出現(xiàn)錯誤,所以我們將HandlerTask定義為static函數(shù),然后再將this指針通過pthread_create參數(shù)傳入進去,就可以避免出現(xiàn)這樣的問題
            pthread_create(&(threads_[i].tid),nullptr,HandlerTask,this);
        }
    }

    //往任務(wù)隊列塞任務(wù)(主線程調(diào)用)
    void Push(const T&t)
    {
        Lock();
        task_.push(t);
        Wakeup();
        UnLock();
    }

    //從任務(wù)隊列獲取任務(wù)(線程池中的線程調(diào)用)
    T Pop()
    {
        T t = task_.front();
        task_.pop();
        return t;
    }

    static ThreadPool<T>* GetInstance()
    {
        if(tp_ == nullptr)//tp_被創(chuàng)建了,就直接返回tp_,不需要讓其他線程再去申請鎖
        {
            pthread_mutex_lock(&lock_);//防止多個線程獲取單例,加上鎖防止被new多次
            if(tp_ == nullptr)
            {
                cout << "log: singleton create done first!" << endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defaultnum):threads_(num)
    {
        pthread_mutex_init(&mutex_,nullptr);
        pthread_cond_init(&cond_,nullptr);    
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator = (const ThreadPool<T> &) = delete;
private:
    vector<ThreadInfo> threads_;//存放線程
    queue<T> task_;//創(chuàng)建任務(wù)隊列

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

template<class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

服務(wù)類新增線程池成員?

我們使用的是單例模式的線程池,在服務(wù)端Start函數(shù)調(diào)用線程池中的GetInstance函數(shù)獲取單例,然后對調(diào)用線程池中封裝的Start函數(shù)創(chuàng)建線程,并指定線程池中的線程數(shù)量(不指定默認(rèn)為5個線程),并完成現(xiàn)成的初始化。這些線程將不斷從任務(wù)隊列中取出任務(wù)進行處理。

當(dāng)服務(wù)進程通過accept函數(shù)獲得客戶端連接請求時,根據(jù)客戶端的套接字、IP地址和端口號構(gòu)建任務(wù),并使用線程池提供的Push接口將其添加到任務(wù)隊列中。

這是一個典型的生產(chǎn)者-消費者模型。服務(wù)進程作為任務(wù)的生產(chǎn)者,而線程池中的線程作為消費者。生產(chǎn)者和消費者在任務(wù)隊列中進行交互,線程池充當(dāng)了他們之間的交易場所。通過這種方式,服務(wù)進程可以快速將任務(wù)添加到隊列中,而線程池中的線程則負責(zé)高效地處理這些任務(wù)。

#pragma once

#include <iostream>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Log.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"

extern Log lg;

using namespace std;

const int defaultfd = -1;
const int backlog = 10; // 但是一般不要設(shè)置的太大
const string defaultip = "0.0.0.0";

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};
class TcpServer;//ThreadData要用到,提前聲明

class ThreadData
{
public:
    ThreadData(int fd,const string& ip,const uint16_t port, TcpServer *t)
    :sockfd(fd),
    clientip(ip),
    clientport(port),
    tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};

class TcpServer
{
public:
    TcpServer(const uint16_t& serverport,const string& ip = defaultip)
        :listensock_(defaultfd),
        port_(serverport),
        ip_(ip)
    {}
    void InitServer()
    {
        //1.創(chuàng)建套接字
        listensock_ = socket(AF_INET,SOCK_STREAM,0);
        if(listensock_ < 0)
        {
           lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
           exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);

        //2.綁定
        struct sockaddr_in local;
        bzero(&local,sizeof(0));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        // local.sin_addr = INADDR_ANY;
        inet_aton(ip_.c_str(),&(local.sin_addr));//換個接口寫一下
        

        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }

        lg(Info, "bind socket success, listensock_: %d", listensock_);

        //3.Tcp是面向連接的,服務(wù)器一般是比較“被動的”,服務(wù)器一直處于一種,一直在等待連接到來的狀態(tài)
        if(listen(listensock_,backlog) < 0)
        {
            lg(Fatal,"listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }
        lg(Info, "listen socket success, listensock_: %d", listensock_);
    }

    void Start()
    {
        // signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信號
        ThreadPool<Task>::GetInstance()->Start();
        lg(Info,"tcpServer is running...");
        for(;;)
        {
            //1.獲取新連接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
            if(sockfd < 0)
            {
                lg(Warning,"accept error, errno: %d ,errstring: %s", errno, strerror(errno));
                continue;//TCP服務(wù)器不會因為某個連接獲取失敗而終止。因此,當(dāng)服務(wù)端獲取連接失敗時,應(yīng)該繼續(xù)嘗試獲取其他連接。
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET,&(client.sin_addr),clientip,sizeof(client));
            lg(Info, "get a new link, listensock_: %d ,%s, %d", sockfd, clientip, clientport);

            //2.根據(jù)新鏈接來進行通信
            // version 3 -- 多線程版本
            // ThreadData* td = new ThreadData(sockfd,clientip,clientport,this);
            // pthread_t tid;
            // pthread_create(&tid,nullptr,Rountine,td);

            // version 4 --- 線程池版本
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);
        }
    }


    ~TcpServer()
    {
        if(listensock_ >= 0) close(listensock_);
    }

private:
    int listensock_;//監(jiān)聽套接字
    uint16_t port_;//端口號
    string ip_;
};

設(shè)計任務(wù)類

我們現(xiàn)在要設(shè)計一個任務(wù)類,該類將包含與特定客戶端相關(guān)的套接字、IP地址和端口號。這些信息將用于標(biāo)識該任務(wù)是為哪個客戶端提供服務(wù),以及對應(yīng)的操作套接字是什么。

任務(wù)類中還應(yīng)包含一個Run方法。當(dāng)線程池中的線程獲取任務(wù)后,它將直接調(diào)用Run方法來處理該任務(wù)。實際上,處理任務(wù)的方法是服務(wù)類中的Service函數(shù)。為了保持軟件分層結(jié)構(gòu),我們不直接將Service函數(shù)放入任務(wù)類中作為Run方法。相反,我們可以為任務(wù)類添加一個仿函數(shù)成員。當(dāng)執(zhí)行任務(wù)類的Run方法來處理任務(wù)時,我們可以以回調(diào)的方式處理該任務(wù)。

此時需要再設(shè)計一個Handler類,在Handler類當(dāng)中對()操作符進行重載,將()操作符的執(zhí)行動作重載為執(zhí)行Service函數(shù)的代碼。

class Handler
{
public:
    void operator()(int sockfd,const string& clientip,const uint16_t& clientport)
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd,buffer,sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;
                cout << "client say#" << buffer << endl;
                string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                write(sockfd,echo_string.c_str(),echo_string.size());
            }
            else if(n == 0)
            {
                lg(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                lg(Warning,"read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        
        }
    }

};

class Task
{
public:
    Task(int sockfd,const string& clientip,const uint16_t& clientport)
    :sockfd_(sockfd),
    clientip_(clientip),
    clientport_(clientport)
    {
    }

    Task()
    {}

    void run()
    {
        handler(sockfd_, clientip_, clientport_); //調(diào)用仿函數(shù)
    }

    void operator()()
    {
        run();
    }

    ~Task()
    {}
private:
    int sockfd_;
    uint16_t clientport_;
    string clientip_;
    Handler handler;
};

注意:當(dāng)任務(wù)隊列中有任務(wù)時,線程池中的線程會創(chuàng)建一個Task對象,并將其作為輸出參數(shù)傳遞給任務(wù)隊列的Pop函數(shù),從而從隊列中獲取任務(wù)。為了方便創(chuàng)建無參的Task對象,Task類需要提供一個無參構(gòu)造函數(shù)。同時,為了能夠通過Pop函數(shù)將Task對象從任務(wù)隊列中彈出,Task類還需要提供一個帶參的構(gòu)造函數(shù)。

實際上,服務(wù)器可以處理各種不同的任務(wù),而不僅僅是簡單的字符串回顯。任務(wù)的具體處理方式是由任務(wù)類中的handler成員決定的。

如果需要服務(wù)器處理其他類型的任務(wù),只需修改Handler類中對()的重載函數(shù)即可。服務(wù)器的初始化、啟動和線程池的代碼無需更改。這種設(shè)計方式實現(xiàn)了通信功能與業(yè)務(wù)邏輯的軟件解耦,使得代碼更加靈活和可擴展。

代碼測試?

此時我們再重新編譯服務(wù)端代碼,并用以下監(jiān)控腳本查看服務(wù)端的各個線程。?

while :; do ps -aL|head -1 && ps -aL| grep tcp_server;echo "####################";sleep 1;done

運行服務(wù)端后,就算沒有客戶端發(fā)來連接請求,此時在服務(wù)端就已經(jīng)有了6個線程,其中有一個是接收新連接的服務(wù)線程,而其余的5個是線程池當(dāng)中為客戶端提供服務(wù)的線程。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

當(dāng)客戶端連接到服務(wù)器時,服務(wù)端的主線程會接收連接請求,并將其封裝為一個任務(wù)對象,然后將其推送到任務(wù)隊列中。線程池中的5個線程會有一個線程從隊列中取出該任務(wù),并執(zhí)行任務(wù)的處理函數(shù),從而為客戶端提供服務(wù)。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

當(dāng)?shù)诙€客戶端發(fā)起連接請求時,服務(wù)端同樣會將該請求封裝為一個任務(wù)對象,并將其放入任務(wù)隊列中。線程池中的線程會從隊列中取出任務(wù)進行處理。由于每個任務(wù)由不同的執(zhí)行流處理,因此兩個客戶端可以同時享受服務(wù),并由不同的線程為其提供服務(wù)。這樣保證了服務(wù)器的并發(fā)處理能力,使得多個客戶端能夠同時得到響應(yīng)。

【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP),Linux網(wǎng)絡(luò),網(wǎng)絡(luò),linux,tcp/ip,c++,服務(wù)器

與之前的情況不同,無論現(xiàn)在有多少客戶端發(fā)起請求,服務(wù)端只有線程池中的5個線程為其提供服務(wù)。線程池中的線程數(shù)量不會因為客戶端數(shù)量的增加而增加,這些線程也不會因為客戶端的斷開而退出。這種設(shè)計確保了服務(wù)器的穩(wěn)定性和性能,同時避免了因線程過多而導(dǎo)致的資源浪費和調(diào)度開銷。?文章來源地址http://www.zghlxwxcb.cn/news/detail-841960.html

到了這里,關(guān)于【Linux網(wǎng)絡(luò)】網(wǎng)絡(luò)編程套接字(TCP)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費用

相關(guān)文章

  • 【Linux】TCP網(wǎng)絡(luò)套接字編程+協(xié)議定制+序列化和反序列化

    【Linux】TCP網(wǎng)絡(luò)套接字編程+協(xié)議定制+序列化和反序列化

    悟已往之不諫,知來者之可追。抓不住的就放手,屬于你的都在路上…… 1. 為了讓我們的代碼更規(guī)范化,所以搞出了日志等級分類,常見的日志輸出等級有DEBUG NORMAL WARNING ERROR FATAL等,再配合上程序運行的時間,輸出的內(nèi)容等,公司中就是使用日志分類的方式來記錄程序的輸

    2024年02月08日
    瀏覽(25)
  • [Linux] 網(wǎng)絡(luò)編程 - 初見TCP套接字編程: 實現(xiàn)簡單的單進程、多進程、多線程、線程池tcp服務(wù)器

    [Linux] 網(wǎng)絡(luò)編程 - 初見TCP套接字編程: 實現(xiàn)簡單的單進程、多進程、多線程、線程池tcp服務(wù)器

    網(wǎng)絡(luò)的上一篇文章, 我們介紹了網(wǎng)絡(luò)變成的一些重要的概念, 以及 UDP套接字的編程演示. 還實現(xiàn)了一個簡單更簡陋的UDP公共聊天室. [Linux] 網(wǎng)絡(luò)編程 - 初見UDP套接字編程: 網(wǎng)絡(luò)編程部分相關(guān)概念、TCP、UDP協(xié)議基本特點、網(wǎng)絡(luò)字節(jié)序、socket接口使用、簡單的UDP網(wǎng)絡(luò)及聊天室實現(xiàn)…

    2024年02月16日
    瀏覽(32)
  • 網(wǎng)絡(luò)編程【TCP流套接字編程】

    網(wǎng)絡(luò)編程【TCP流套接字編程】

    目錄 TCP流套接字編程 1.ServerSocket API 2.Socket API 3.TCP中的長短連接 4.回顯程序(短連接) 5.服務(wù)器和客戶端它們的交互過程 6.運行結(jié)果及修改代碼 ? ??兩個核心: ServerSocket? ? ?Socket 1.ServerSocket API ? ServerSocket 是創(chuàng)建?TCP服務(wù)端Socket的API ServerSocket 構(gòu)造方法: ServerSocket 方法 :

    2023年04月12日
    瀏覽(573)
  • 網(wǎng)絡(luò)編程套接字( TCP )

    網(wǎng)絡(luò)編程套接字( TCP )

    目錄 1、實現(xiàn)一個TCP網(wǎng)絡(luò)程序(單進程版) ????????1.1、服務(wù)端serverTcp.cc文件 ?????????????????服務(wù)端創(chuàng)建套接字 ?????????????????服務(wù)端綁定 ?????????????????服務(wù)端監(jiān)聽 ?????????????????服務(wù)端獲取連接 ?????????????????服務(wù)

    2024年01月17日
    瀏覽(1816)
  • 【JaveEE】網(wǎng)絡(luò)編程之TCP套接字、UDP套接字

    【JaveEE】網(wǎng)絡(luò)編程之TCP套接字、UDP套接字

    目錄 1.網(wǎng)絡(luò)編程的基本概念 1.1為什么需要網(wǎng)絡(luò)編程? 1.2服務(wù)端與用戶端 1.3網(wǎng)絡(luò)編程五元組? 1.4套接字的概念 2.UDP套接字編程 2.1UDP套接字的特點 ?2.2UDP套接字API 2.2.1DatagramSocket類 2.2.2DatagramPacket類? 2.2.3基于UDP的回顯程序 2.2.4基于UDP的單詞查詢? 3.TCP套接字編程 3.1TCP套接字的特

    2023年04月13日
    瀏覽(915)
  • 【JavaEE】網(wǎng)絡(luò)編程之TCP套接字、UDP套接字

    【JavaEE】網(wǎng)絡(luò)編程之TCP套接字、UDP套接字

    目錄 1.網(wǎng)絡(luò)編程的基本概念 1.1為什么需要網(wǎng)絡(luò)編程? 1.2服務(wù)端與用戶端 1.3網(wǎng)絡(luò)編程五元組? 1.4套接字的概念 2.UDP套接字編程 2.1UDP套接字的特點 ?2.2UDP套接字API 2.2.1DatagramSocket類 2.2.2DatagramPacket類? 2.2.3基于UDP的回顯程序 2.2.4基于UDP的單詞查詢? 3.TCP套接字編程 3.1TCP套接字的特

    2023年04月20日
    瀏覽(120)
  • 【網(wǎng)絡(luò)編程】網(wǎng)絡(luò)編程套接字(三)TCP網(wǎng)絡(luò)程序

    【網(wǎng)絡(luò)編程】網(wǎng)絡(luò)編程套接字(三)TCP網(wǎng)絡(luò)程序

    與前邊的UDP網(wǎng)絡(luò)程序相同,創(chuàng)建套接字的接口都是socket,下邊對socket接口進行介紹: 協(xié)議家族選擇AF_INET,因為我們要進行網(wǎng)絡(luò)通信。 而第二個參數(shù),為服務(wù)類型,傳入SOCK_STREAM,我們編寫TCP程序,所以要選擇流式的服務(wù)。 第三個參數(shù)默認(rèn)傳入0,由前兩個參數(shù)就可以推出這是

    2024年02月16日
    瀏覽(92)
  • 網(wǎng)絡(luò)編程套接字應(yīng)用分享【Linux &C/C++ 】【UDP應(yīng)用 | TCP應(yīng)用 | TCP&線程池小項目】

    網(wǎng)絡(luò)編程套接字應(yīng)用分享【Linux &C/C++ 】【UDP應(yīng)用 | TCP應(yīng)用 | TCP&線程池小項目】

    目錄 前提知識 1. 理解源ip,目的ip和Macip 2. 端口號 3. 初識TCP,UDP協(xié)議 4.?網(wǎng)絡(luò)字節(jié)序 5. socket 編程 sockaddr類型? 一,基于udp協(xié)議編程? 1. socket——創(chuàng)建套接字 2. bind——將套接字強綁定? 3. recvfrom——接受數(shù)據(jù) 4. sendto——發(fā)出信息 ?遇到的問題 (1. 云服務(wù)器中以及無法分配I

    2024年04月08日
    瀏覽(27)
  • 網(wǎng)絡(luò)編程套接字之三【TCP】

    網(wǎng)絡(luò)編程套接字之三【TCP】

    目錄 1. ServerSocket API(給服務(wù)器端使用的類) 2. Socket API(既給服務(wù)器使用,也給客戶端使用) 3. 寫TCP回顯—服務(wù)器 4. 使用線程池后的TCP服務(wù)器代碼(最終) 5. 寫回顯-客戶端 6. TCP回顯—客戶端代碼 7. 運行回顯服務(wù)器和客戶端 TCP流套接字編程 ?ServerSocket 是創(chuàng)建TCP服務(wù)端Socket的

    2024年01月19日
    瀏覽(91)
  • 「網(wǎng)絡(luò)編程」第二講:socket套接字(四 - 完結(jié))_ Linux任務(wù)管理與守護進程 | TCP協(xié)議通訊流程

    「網(wǎng)絡(luò)編程」第二講:socket套接字(四 - 完結(jié))_ Linux任務(wù)管理與守護進程 | TCP協(xié)議通訊流程

    「前言」文章是關(guān)于網(wǎng)絡(luò)編程的socket套接字方面的,上一篇是網(wǎng)絡(luò)編程socket套接字(三),這篇續(xù)上篇文章的內(nèi)容,下面開始講解!? 「歸屬專欄」網(wǎng)絡(luò)編程 「主頁鏈接」個人主頁 「筆者」楓葉先生(fy) 「楓葉先生有點文青病」「句子分享」 Time?goes?on?and?on,?never?to?an?

    2024年02月10日
    瀏覽(47)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包