Linux之TCP網(wǎng)絡(luò)套接字詳解
一、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下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ī),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08

