欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Linux之TCP網(wǎng)絡(luò)套接字詳解

 更新時(shí)間:2025年08月11日 10:02:29   作者:每天敲200行代碼  
TCP socket API包含創(chuàng)建、綁定、監(jiān)聽、接受連接、連接等步驟,需三次握手建立連接,四次揮手?jǐn)嚅_,與UDP相比,TCP是面向連接的可靠傳輸協(xié)議,基于字節(jié)流進(jìn)行數(shù)據(jù)交換,確保雙向通信可靠性

一、TCP socket API 詳解

1、插件套接字 socket

在通信之前要先把網(wǎng)卡文件打開。

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

成功則返回打開的文件描述符(指向網(wǎng)卡文件),失敗返回-1。 

這個(gè)函數(shù)的作用是打開一個(gè)文件,把文件和網(wǎng)卡關(guān)聯(lián)起來。 

  • domain:一個(gè)域,標(biāo)識了這個(gè)套接字的通信類型(網(wǎng)絡(luò)或者本地)。

(只需要關(guān)注上面兩個(gè)類,第一個(gè) AF_UNIX 表示本地通信,而 AF_INET 表示網(wǎng)絡(luò)通信。

  • type:套接字提供服務(wù)的類型。

  • protocol:想使用的協(xié)議,默認(rèn)為 0 即可,因?yàn)榍懊娴膬蓚€(gè)參數(shù)決定了,就已經(jīng)決定了是 TCP 還是 UDP 協(xié)議了。

從這里我們就可以聯(lián)想到系統(tǒng)中的文件操作,未來各種操作都要通過這個(gè)文件描述符,所以在服務(wù)端類中還需要一個(gè)成員變量表示文件描述符。

2、綁定 bind

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

 

bind() 成功返回 0,失敗返回 -1。

  • socket:創(chuàng)建套接字的返回值。
  • address:通用結(jié)構(gòu)體(前面有詳細(xì)介紹)。
  • address_len:傳入結(jié)構(gòu)體的長度。

所以我們需要先定義一個(gè)address_in 結(jié)構(gòu)體填充數(shù)據(jù),再傳遞進(jìn)去。

3、設(shè)置監(jiān)聽狀態(tài) listen

因?yàn)?TCP 是面向連接的,當(dāng)我們正式通信的時(shí)候,需要先建立連接,那么 TCP 跟 UDP 的不同在這里就體現(xiàn)了出來。要把 socket 套接字的狀態(tài)設(shè)置為 listen 狀態(tài),只有這樣才能一直獲取新鏈接,接收新的鏈接請求。

舉例幫助理解:我們買東西如果出現(xiàn)了問題會去找客服,如果客服不在,那么就無法回復(fù)我們,所以就規(guī)定了客服在工作的時(shí)候必須要時(shí)刻接收回復(fù)消息,那么這個(gè)客服所處的狀態(tài)就叫做監(jiān)聽狀態(tài)。

 

關(guān)于第二個(gè)參數(shù):backlog,后邊講 TCP 協(xié)議參數(shù)時(shí)會再詳細(xì)介紹,目前先直接用。( 一般不能太大,也不能太?。?/p>

listen() 成功返回 0,失敗返回 -1。 

  • listen() 聲明 sockfd 處于監(jiān)聽狀態(tài),并且最多允許有 backlog 個(gè)客戶端處于連接等待狀態(tài),如果接收到更多的連接請求就忽略,這里設(shè)置不會太大(一般是 5)。

創(chuàng)建套接字成功,套接字對應(yīng)的文件描述符值是 3,為什么是 3 呢?

因?yàn)楫?dāng)前對應(yīng)的文件描述符返回的套接字本身就是一個(gè)文件描述符,0、1、2 被占用,再創(chuàng)建一個(gè)文件,對應(yīng)的就是 3。

4、獲取新鏈接 accept

前面初始化完成,現(xiàn)在就是要開始運(yùn)行服務(wù)端。TCP 不能直接發(fā)送數(shù)據(jù),因?yàn)樗敲嫦蜴溄拥模?strong>必須要先建立鏈接。

成功返回一個(gè)文件描述符,失敗返回 -1。

  • sockfd:文件描述符,找到套接字。
  • addr:輸入輸出型參數(shù),是一個(gè)結(jié)構(gòu)體,用來獲取客戶端的信息。
  • addrlen:輸入輸出型參數(shù),客戶端傳過來的結(jié)構(gòu)體大小。 

三次握手完成后,服務(wù)器調(diào)用 accept() 接受連接。

如果服務(wù)器調(diào)用 accept() 時(shí)還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來。

addr 是一個(gè)傳出參數(shù),accept() 返回時(shí)傳出客戶端的地址和端口號。

如果給 addr 參數(shù)傳 NULL,表示不關(guān)心客戶端的地址。

addrlen 參數(shù)是一個(gè)傳入傳出參數(shù) (value-result argument),傳入的是調(diào)用者提供的,緩沖區(qū) addr 的長度以避免緩沖區(qū)溢出問題,傳出的是客戶端地址結(jié)構(gòu)體的實(shí)際長度(有可能沒有占滿調(diào)用者提供的緩沖區(qū))。

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

sockfd 本來就是一個(gè)文件描述符,那么這個(gè)返回的文件描述符是什么呢?

  • 舉例幫助理解:我們?nèi)コ燥垥r(shí),會發(fā)現(xiàn)一些店鋪的門口有工作人員來招攬顧客,他將我們領(lǐng)進(jìn)門店之后,他會站在門口繼續(xù)招攬顧客,而我們會由里面的服務(wù)員來招待我們,給我們提供服務(wù)。
  • 這里攬客的工作人員指的就是 sockfd,而店里面的服務(wù)員就是返回值的文件描述符。也就是說,sockfd 的作用就是把鏈接從底層獲取上來,而返回值的作用就是跟客戶端通信。

那么我們就知道了,成員變量中的 _sock 并不是通信用的套接字,而是獲取鏈接的套接字。為了方便觀察,我們可以把前面所有的 _sock 換成 _listensock。

客服端的整體代碼如下:

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <cerrno>
#include <string>
#include "log.hpp"
#include <unistd.h>

Log lg;
class TcpServer
{
public:
    // 下面這個(gè)構(gòu)造函數(shù)時(shí)全缺省的,就不需要這個(gè)默認(rèn)構(gòu)造了
    // TcpServer()
    // {
    // }
    // 第一個(gè)參數(shù)時(shí)listensock,但是這個(gè)時(shí)在init的時(shí)候socket接口創(chuàng)建的套接字,你如果傳參的話,第一個(gè)傳遞的應(yīng)該時(shí)監(jiān)聽套接字,而不是一個(gè)端口號,
    // 你在調(diào)用的時(shí)候,傳遞的時(shí)一個(gè)端口號,所以這里時(shí)有問題的,
    // TcpServer(int sock = 0, uint16_t port = 8080, std::string ip = "0.0.0.0")
    //     : listensock_(sock), port_(port), ip_(ip)
    // {
    // }
    TcpServer(uint16_t port = 8080, std::string ip = "0.0.0.0")
        : listensock_(-1), port_(port), ip_(ip)
    {
    }
    void Init()
    {
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            lg(Fatal, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        lg(Info, "Create socket success,sockfd:%d", listensock_);
        struct sockaddr_in local;
        bzero(&local, 0);
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = inet_addr(ip_.c_str());
        socklen_t len = sizeof(local);
        if (bind(listensock_, (const sockaddr *)&local, len) < 0)
        {
            lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(3);
        }
        lg(Info, "bind success!");
        if (listen(listensock_, 5) < 0)
        {
            lg(Fatal, "listen error, errno: %d, err string: %s", errno, strerror(errno));
            exit(4);
        }
    }
    void start()
    {
        lg(Info, "tcpServer is running....");
        // 1. 獲取新連接
        char buffer[1024];
        struct sockaddr_in client;
        memset(&client, 0, sizeof(client));
        socklen_t len = sizeof(client);

        while (true)
        {
            // 這里是由問題的,因?yàn)槟惬@取一個(gè)連接之后,應(yīng)該是多次進(jìn)行通信,而不是通信一次之后,在獲取連接,因?yàn)樯弦粋€(gè)連接還沒有處理完呢
            int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            string clientip = inet_ntoa(client.sin_addr);
            // 連接成功后,為客戶端提供服務(wù)
            while (1)
            {
                // 連接成功之后,多次處理這個(gè)連接的請求
                ssize_t size = read(sockfd, buffer, sizeof(buffer) - 1);
                if (size > 0)
                {
                    buffer[size] = 0;
                    cout << '[' << clientip << ':' << clientport << "]# " << buffer << endl;
                    write(sockfd, buffer, size);
                }
                else if (size == 0)
                {
                    // 如果讀取到0的時(shí)候,就是對方斷開了,
                    // 需要做的事情就是關(guān)閉套接字,然后跳出循環(huán),獲取下一個(gè)連接
                    close(sockfd);
                    break;
                }
                else
                {
                    // 如果是小于0的話,那么就說明read這個(gè)錯(cuò)誤失敗了,也需要關(guān)閉連接
                    close(sockfd);
                    break;
                }
            }
        }
        // while (true)
        // {
        //     // 這里是由問題的,因?yàn)槟惬@取一個(gè)連接之后,應(yīng)該是多次進(jìn)行通信,而不是通信一次之后,在獲取連接,因?yàn)樯弦粋€(gè)連接還沒有處理完呢
        //     int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
        //     if (sockfd < 0)
        //     {
        //         lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
        //         continue;
        //     }
        //     uint16_t clientport = ntohs(client.sin_port);
        //     string clientip = inet_ntoa(client.sin_addr);
        //     // 連接成功后,為客戶端提供服務(wù)
        //     ssize_t size = read(sockfd, buffer, sizeof(buffer) - 1);
        //     if (size > 0)
        //     {
        //         buffer[size] = 0;
        //         cout << '[' << clientip << ':' << clientport << "]# " << buffer << endl;
        //         write(sockfd, buffer, size);
        //         std::cout << 1 <<std::endl;
        //     }
        // }
    }

public:
    int listensock_;
    uint16_t port_;
    std::string ip_;
};

 

5、發(fā)起鏈接 connect

  • 客戶端需要調(diào)用 connect() 連接服務(wù)器。
  • connect 和 bind 的參數(shù)形式一致,區(qū)別在于 bind 的參數(shù)是自己的地址,而 connect 的參數(shù)是對方的地址。

 

connect() 成功返回 0,出錯(cuò)返回 -1。

這里的 addr 和 addrlen 填入的是服務(wù)端信息。 

在 UDP 通信中,客戶端在 sendto 時(shí)會自動綁定 IP 和 port,而 TCP 就是在 connect 的時(shí)候進(jìn)行綁定。因?yàn)?connect 是系統(tǒng)調(diào)用接口,所以在調(diào)用 connect 時(shí)會自動的給綁定當(dāng)前客戶端的 ip 和 port,進(jìn)而可以讓我們在后續(xù)使用 sockfd 進(jìn)行通信。

客戶端.cpp

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
using std::cout;
using std::endl;

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

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

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 1;
    }
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect error..." << std::endl;
        exit(2);
    }
    cout << "connect sunccess!" << endl;
    while (true)
    {
        while (true)
        {
            std::string message;
            std::cout << "Please Enter# ";
            std::getline(std::cin, message);

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

            char inbuffer[4096];
            n = read(sockfd, inbuffer, sizeof(inbuffer));
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << inbuffer << std::endl;
            }
            else
            {
                // break;
            }
        }

        close(sockfd);
    }

    return 0;
}

運(yùn)行結(jié)果如下:

二、TCP 協(xié)議通訊流程

下圖是基于 TCP 協(xié)議的客戶端/服務(wù)器程序的一般流程:

1、服務(wù)器初始化

  • 調(diào)用 socket,創(chuàng)建文件描述符。
  • 調(diào)用 bind,將當(dāng)前的文件描述符和 ip/port 綁定在一起,如果這個(gè)端口已經(jīng)被其他進(jìn)程占用了,就會 bind 失敗。
  • 調(diào)用 listen,聲明當(dāng)前這個(gè)文件描述符作為一個(gè)服務(wù)器的文件描述符,為后面的 accept 做好準(zhǔn)備。
  • 調(diào)用 accecpt,并阻塞,等待客戶端連接過來。

2、建立連接的過程

  • 調(diào)用 socket,創(chuàng)建文件描述符。
  • 調(diào)用 connect,向服務(wù)器發(fā)起連接請求。
  • connect 會發(fā)出 SYN 段并阻塞等待服務(wù)器應(yīng)答(第一次)。
  • 服務(wù)器收到客戶端的 SYN,會應(yīng)答一個(gè) SYN-ACK 段表示 “同意建立連接”(第二次)。
  • 客戶端收到 SYN-ACK 后會從 connect() 返回,同時(shí)應(yīng)答一個(gè) ACK 段(第三次)。

TCP 是面向連接的通信協(xié)議,在通信之前需要進(jìn)行 3 次握手,來進(jìn)行連接的建立。這個(gè)建立連接的過程通常稱為 三次握手 

3、數(shù)據(jù)傳輸?shù)倪^程

  • 建立連接后,TCP 協(xié)議提供全雙工的通信服務(wù)。所謂全雙工的意思是,在同一條連接中, 同一時(shí)刻,通信雙方可以同時(shí)寫數(shù)據(jù)。相對的概念叫做半雙工,同一條連接在同一時(shí)刻,只能由一方來寫數(shù)據(jù)。
  • 服務(wù)器從 accept() 返回后立刻調(diào)用 read(),讀 socket 就像讀管道一樣,如果沒有數(shù)據(jù)到達(dá)就阻塞等待。
  • 這時(shí)客戶端調(diào)用 write() 發(fā)送請求給服務(wù)器, 服務(wù)器收到后從 read() 返回,對客戶端的請求進(jìn)行處理,在此期間客戶端調(diào)用 read()阻塞等待服務(wù)器的應(yīng)答;
  • 服務(wù)器調(diào)用 write() 將處理結(jié)果發(fā)回給客戶端,再次調(diào)用 read() 阻塞等待下一條請求。
  • 客戶端收到后從 read() 返回,發(fā)送下一條請求,如此循環(huán)下去。

4、斷開連接的過程

  • 如果客戶端沒有更多的請求了,就調(diào)用 close() 關(guān)閉連接,客戶端會向服務(wù)器發(fā)送 FIN 段(第一次)。
  • 此時(shí)服務(wù)器收到 FIN 后,會回應(yīng)一個(gè) ACK,同時(shí) read 會返回 0(第二次)。
  • read 返回之后,服務(wù)器就知道客戶端關(guān)閉了連接, 也調(diào)用 close 關(guān)閉連接,這個(gè)時(shí)候服務(wù)器會向客戶端發(fā)送一個(gè) FIN(第三次)。
  • 客戶端收到 FIN,再返回一個(gè) ACK 給服務(wù)器(第四次)。

當(dāng) TCP 斷開連接這個(gè)斷開連接的過程 , 通常稱為 四次揮手 。

為什么是四次揮手呢?

  • 因?yàn)?TCP 是基于確定應(yīng)答來保證單項(xiàng)可靠性的,如果對方給我發(fā)消息,我也給對方進(jìn)行應(yīng)答,那么就能夠保證雙向的可靠性。所以,發(fā)出去的斷開連接的過程需要應(yīng)答。
  • 當(dāng)客戶端斷開連接時(shí),要保證客戶端到服務(wù)的連接被成功關(guān)閉,所以需要調(diào)用一次,而服務(wù)端除了要釋放自身創(chuàng)建好的文件描述符,也要關(guān)閉從服務(wù)端到客戶端對應(yīng)的連接,因?yàn)殡p方都要調(diào)用 close() 各自兩次,那么一來一來就緒各自需要兩次揮手,加起來就是四次揮手。

三、總結(jié)

對比 UDP 服務(wù)器,TCP 服務(wù)器多了獲取新鏈接和監(jiān)聽的操作,而因?yàn)?TCP 是面向字節(jié)流的,所以接收和發(fā)送數(shù)據(jù)都是 IO 操作,也就是文件操作。

TCP 和 UDP 對比

  • 可靠傳輸 VS 不可靠傳輸
  • 有連接 VS 無連接
  • 字節(jié)流 VS 數(shù)據(jù)報(bào)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • linux下安裝PyQt5+qt-esigner教程

    linux下安裝PyQt5+qt-esigner教程

    文章介紹了在Linux下安裝PyQt5和Qt Designer,并詳細(xì)描述了如何將.ui文件編譯為.py文件,以及如何編寫主文件來運(yùn)行生成的.py文件,步驟包括安裝PyQt5和Qt Designer、使用pyuic命令編譯.ui文件、在.bashrc中添加快捷方式,并編寫主文件導(dǎo)入生成的.py文件
    2025-01-01
  • Linux之常見漏洞修復(fù)

    Linux之常見漏洞修復(fù)

    這篇文章主要介紹了Linux中的常見漏洞修復(fù),具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • Linux 下rpm命令參數(shù)詳解

    Linux 下rpm命令參數(shù)詳解

    linux下的rpm常用命令,主要是用來安裝rmp包,是linux下比較方便的東西
    2008-06-06
  • Linux下的進(jìn)程控制解讀

    Linux下的進(jìn)程控制解讀

    這篇文章主要介紹了Linux下的進(jìn)程控制,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-06-06
  • Apache 防盜鏈的技術(shù)小結(jié)

    Apache 防盜鏈的技術(shù)小結(jié)

    Apache 防盜鏈的第一種實(shí)現(xiàn)方法,可以用 rewrite 實(shí)現(xiàn)。
    2010-12-12
  • Linux下sersync數(shù)據(jù)實(shí)時(shí)同步

    Linux下sersync數(shù)據(jù)實(shí)時(shí)同步

    這篇文章主要為大家詳細(xì)介紹了Linux下sersync數(shù)據(jù)實(shí)時(shí)同步的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-02-02
  • Zabbix基于snmp實(shí)現(xiàn)監(jiān)控linux主機(jī)

    Zabbix基于snmp實(shí)現(xiàn)監(jiān)控linux主機(jī)

    這篇文章主要介紹了Zabbix基于snmp實(shí)現(xiàn)監(jiān)控linux主機(jī),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-08-08
  • VMware下Ubuntu16.04鏡像完整安裝教程

    VMware下Ubuntu16.04鏡像完整安裝教程

    這篇文章主要為大家詳細(xì)介紹了VMware下Ubuntu16.04鏡像完整安裝教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-06-06
  • Vagrant基本命令使用詳解

    Vagrant基本命令使用詳解

    本篇文章主要介紹了Vagrant基本命令詳解,現(xiàn)在分享給大家,也給大家做個(gè)參考。感興趣的小伙伴們可以參考一下。
    2016-11-11
  • Linux系統(tǒng)中的軟連接管理詳解

    Linux系統(tǒng)中的軟連接管理詳解

    在 Linux 系統(tǒng)中,軟連接(或稱符號鏈接)是一種重要的文件系統(tǒng)功能,它允許用戶創(chuàng)建指向其他文件或目錄的快捷方式,通過軟連接,用戶可以方便地訪問常用文件、程序或目錄,而無需每次都輸入完整路徑,本文將詳細(xì)給大家介紹了Linux系統(tǒng)中的軟連接管理
    2024-11-11

最新評論