一、網絡架構
C/S (client/server? 客戶端/服務器):由客戶端和服務器端兩個部分組成。客戶端通常是用戶使用的應用程序,負責提供用戶界面和交互邏輯 ,接收用戶輸入,向服務器發送請求,并展示服務器返回的結果;服務器端是提供服務的程序,一般部署在性能較強的計算機上,負責處理數據存儲、業務邏輯計算等,監聽特定端口等待客戶端請求,接收到請求后進行處理并返回結果。
B/S(browser/server? 瀏覽器/服務器):基于瀏覽器和服務器,客戶端通過通用的瀏覽器(如 Chrome、Firefox、等)來訪問應用程序。服務器端包括 Web 服務器和數據庫服務器等,負責處理業務邏輯、存儲和管理數據。
P2P(peer to peer 點對點):是一種去中心化的網絡架構,網絡中的節點(計算機)地位對等,不存在專門的中心服務器。每個節點既可以作為客戶端向其他節點請求服務或資源,也可以作為服務器為其他節點提供服務或資源。
二、C/S 和 B/S 對比:
C/S架構 | B/S架構 | |
客戶端 | 需要安裝專用的客戶端軟件 | 無需安裝客戶端,直接通過瀏覽器訪問 |
協議 | 可自定義私有協議(如游戲使用二進制協議),靈活性高。 | 基于HTTP/HTTPS 協議,需遵循 Web 標準(如 RESTful 接口)。 |
功能 | ||
資源 | 客戶端分擔部分計算壓力,服務器專注數據處理,適合高并發、大數據量場景。 | 所有邏輯在服務器端執行,需應對大量 HTTP 請求,對服務器性能要求高。 |
三、C/S通信流程
? ? ? ? ? ? ? ? ? ? ? ?
服務器端流程
- 創建套接字(socket ()):調用?
socket()
?函數創建一個套接字,這是進行網絡通信的基礎。它會返回一個套接字描述符,后續操作將基于這個描述符進行。該函數確定通信的協議族(如 AF_INET 表示 IPv4)、套接字類型(如 SOCK_STREAM 表示 TCP 流套接字)和協議(通常為 0,由系統根據前兩個參數自動選擇合適協議)。- 綁定地址和端口(bind ()):使用?
bind()
?函數將創建的套接字與特定的 IP 地址和端口號綁定。這樣服務器就能明確在哪個地址和端口上監聽客戶端的連接請求。需要提供套接字描述符、指向包含 IP 地址和端口號信息的結構體指針,以及該結構體的大小。- 監聽連接(listen ()):調用?
listen()
?函數使服務器進入監聽狀態,它會為套接字創建一個等待連接的隊列,參數包括套接字描述符和隊列的最大長度。這一步告訴系統開始接受客戶端的連接請求。- 接受連接(accept ()):
accept()
?函數會阻塞(暫停執行),直到有客戶端發起連接請求。一旦有客戶端連接,它會創建一個新的套接字描述符用于與該客戶端進行通信,同時返回客戶端的地址信息。原來監聽的套接字仍然保持監聽狀態,繼續接受其他客戶端的連接。- 讀取數據(read ()):服務器通過新創建的與客戶端通信的套接字描述符,使用?
read()
?函數讀取客戶端發送過來的數據。read()
?函數從套接字接收數據并存儲到指定的緩沖區中,返回實際讀取的字節數。- 處理請求:服務器對讀取到的數據進行相應的處理,例如解析請求內容、查詢數據庫、進行業務邏輯計算等。
- 寫入數據(write ()):處理完請求后,服務器使用?
write()
?函數將處理結果(響應數據)通過套接字發送回客戶端。write()
?函數將緩沖區中的數據寫入套接字,發送給客戶端。- 再次讀取數據(read ()):服務器可能再次調用?
read()
?函數,等待接收客戶端后續可能發送的數據,比如新的請求或確認信息等。- 關閉連接(close ()):通信結束后,服務器調用?
close()
?函數關閉與客戶端通信的套接字,釋放相關資源。客戶端流程
- 創建套接字(socket ()):與服務器端一樣,客戶端首先調用?
socket()
?函數創建一個套接字,用于后續的網絡通信,返回套接字描述符。- 建立連接(connect ()):客戶端使用?
connect()
?函數嘗試與服務器建立連接。需要指定要連接的服務器的 IP 地址和端口號,以及套接字描述符。如果服務器處于監聽狀態并且接受連接,連接就會成功建立;否則可能會返回錯誤。- 寫入數據(write ()):連接建立后,客戶端調用?
write()
?函數向服務器發送請求數據,將請求內容寫入套接字,發送給服務器。- 讀取數據(read ()):客戶端調用?
read()
?函數從套接字讀取服務器返回的響應數據,將數據存儲到指定的緩沖區中。- 關閉連接(close ()):客戶端完成與服務器的通信后,調用?
close()
?函數關閉套接字,釋放資源。整體交互過程
- 服務器端先完成初始化(創建套接字、綁定、監聽),進入等待客戶端連接的狀態。
- 客戶端創建套接字并嘗試連接服務器,連接成功后,客戶端向服務器發送請求數據。
- 服務器接收請求數據,處理后向客戶端發送響應數據。
- 客戶端接收響應數據,雙方通信結束后各自關閉套接字,釋放資源。
?注意:pid(進程ID)只能在本機范圍內發送,兩臺主機間無法直接發送
四、TCP通信的特點
- 有鏈接:并非一上來就直接進行傳輸,要先通過網絡結點進行直接或者間接的連接,然后通過創建一些函數,連接起來(這條鏈路在通信過程中一直建立著)。
- TCP是一種可靠傳輸:
- 應答機制:在 TCP 通信里,每次接收方收到數據,都會給發送方發送一個應答報文(ACK) 。TCP 通過給每個數據段編號(序列號),接收方根據收到的數據段序列號,在應答報文中用確認序號告知發送方哪些數據已正確接收。例如發送方發送了編號為 1 - 1000 的字節數據,接收方若正確收到,就在應答報文中帶上確認序號 1001(表示期望接收的下一個字節編號),告知發送方 1 - 1000 已正確接收 。
- 超時重傳:發送方發送數據后,會設定一個超時時間,若在該時間內未收到接收方的 ACK 應答報文,不管是數據包丟失還是 ACK 確認應答丟失,發送方都認為數據傳輸失敗,會重新發送數據 。比如網絡擁堵導致數據包在傳輸途中滯留,超過超時時間仍未到達接收方,或者接收方發送的 ACK 在返回途中丟失,發送方都感知不到數據已被接收,就會觸發超時重傳?
- 全雙工:通信雙方都具備獨立的發送和接收通道,發送數據同時可接收數據。比如在網絡通信中,網卡支持全雙工,數據發送線和接收線各自獨立工作 ;發送和接收操作瞬時同步進行。以電話通信為例,通話時雙方說話和聽到對方聲音同步,語音信號在兩個方向同時傳輸 。
- 連續:TCP 將數據視為無邊界的連續字節流,發送方按順序逐字節傳輸,接收方按序列號重組為連續的數據流。
- 有順序:TCP 為每個字節數據分配唯一序列號,接收方按序列號重組數據,確保字節流順序正確;接收方通過 ACK 報文告知發送方已收到的數據序號,發送方僅傳輸未確認的分組,避免亂序。(udp本身不保證)。
五、“三次握手四次揮手 ”
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
?三次握手:
? ? ? ?三次握手其實就是指建立一個TCP連接時,需要客戶端和服務器總共發送3個包。進行三次握手的主要作用就是為了確認雙方的接收能力和發送能力是否正常、指定自己的初始化序列號為后面的可靠性傳送做準備。實質上其實就是連接服務器指定端口,建立TCP連接,并同步連接雙方的序列號和確認號,交換TCP窗口大小信息。
剛開始客戶端處于 Closed 的狀態,服務端處于 Listen 狀態。
在socket編程中,客戶端執行connect()時,將觸發三次握手:
- 第一次握手:客戶端給服務端發一個 SYN 報文,并指明客戶端的初始化序列號 ISN。此時客戶端處于 SYN_SENT 狀態?。首部的同步位SYN=1,初始序號seq=x,SYN=1的報文段不能攜帶數據,但要消耗掉一個序號。
- 第二次握手:服務器收到客戶端的 SYN 報文之后,會以自己的 SYN 報文作為應答,并且也是指定了自己的初始化序列號 ISN(s)。同時會把客戶端的 ISN + 1 作為ACK 的值,表示自己已經收到了客戶端的 SYN,此時服務器處于 SYN_RCVD 的狀態。在確認報文段中SYN=1,ACK=1,確認號ack=x+1,初始序號seq=y。
- 第三次握手:客戶端收到 SYN 報文之后,會發送一個 ACK 報文,當然,也是一樣把服務器的 ISN + 1 作為 ACK 的值,表示已經收到了服務端的 SYN 報文,此時客戶端處于 ESTABLISHED 狀態。服務器收到 ACK 報文之后,也處于 ESTABLISHED 狀態,此時,雙方已建立起了連接。確認報文段ACK=1,確認號ack=y+1,序號seq=x+1(初始為seq=x,第二個報文段所以要+1),ACK報文段可以攜帶數據,不攜帶數據則不消耗序號。
?
四次揮手:?
? ? ? ? 建立一個連接需要三次握手,而終止一個連接要經過四次揮手(也有將四次揮手叫做四次握手的)。這由TCP的半關閉(half-close)造成的。所謂的半關閉,其實就是TCP提供了連接的一端在結束它的發送后還能接收來自另一端數據的能力。
TCP 連接的拆除需要發送四個包,因此稱為四次揮手(Four-way handshake),客戶端或服務端均可主動發起揮手動作,
剛開始雙方都處于ESTABLISHED 狀態,假如是客戶端先發起關閉請求。四次揮手的過程如下,在socket編程中,任何一方執行close()操作即可產生揮手操作:
- 第一次揮手:客戶端發送一個 FIN 報文,報文中會指定一個序列號。此時客戶端處于 FIN_WAIT1 狀態。
? ? ? ? 即發出連接釋放報文段(FIN=1,序號seq=u),并停止再發送數據,主動關閉TCP連? ? ? ? ? ? 接,進入FIN_WAIT1(終止等待1)狀態,等待服務端的確認。
- 第二次揮手:服務端收到 FIN 之后,會發送 ACK 報文,且把客戶端的序列號值 +1 作為 ACK 報文的序列號值,表明已經收到客戶端的報文了,此時服務端處于 CLOSE_WAIT 狀態。
? ? ? ?即服務端收到連接釋放報文段后即發出確認報文段(ACK=1,確認號ack=u+1,序號? ? ? ? ? ? ?seq=v),服務端進入CLOSE_WAIT(關閉等待)狀態,此時的TCP處于半關閉狀態,? ? ? ? ?客戶端到服務端的連接釋放。客戶端收到服務端的確認后,進入FIN_WAIT2(終止等待? ? ? ? ? 2)狀態,等待服務端發出的連接釋放報文段。
- 第三次揮手:如果服務端也想斷開連接了,和客戶端的第一次揮手一樣,發給 FIN 報文,且指定一個序列號。此時服務端處于 LAST_ACK 的狀態。
? ? ? ?即服務端沒有要向客戶端發出的數據,服務端發出連接釋放報文段(FIN=1,ACK=1,? ? ? ? ?序號seq=w,確認號ack=u+1),服務端進入LAST_ACK(最后確認)狀態,等待客戶? ? ? ? ?端的確認。
- 第四次揮手:客戶端收到 FIN 之后,一樣發送一個 ACK 報文作為應答,且把服務端的序列號值 +1 作為自己 ACK 報文的序列號值,此時客戶端處于 TIME_WAIT 狀態。需要過一陣子以確保服務端收到自己的 ACK 報文之后才會進入 CLOSED 狀態,服務端收到 ACK 報文之后,就處于關閉連接了,處于 CLOSED 狀態。
? ? ? ?即客戶端收到服務端的連接釋放報文段后,對此發出確認報文段(ACK=1,seq=u+1,? ? ? ? ?ack=w+1),客戶端進入TIME_WAIT(時間等待)狀態。此時TCP未釋放掉,需要經過
? ? ? ?時間等待計時器設置的時間2MSL后,客戶端才進入CLOSED狀態。
特性: tcp也叫流式套接字????????
六、TCP服務端相關函數
socket()? ? 創建套接字:
? ? ? ? ? ? ? ?
- 參數:
domain
:協議族(如AF_INET
表示 IPv4,AF_INET6
表示 IPv6)。type
:套接字類型(如SOCK_STREAM
表示 TCP 流式套接字,SOCK_DGRAM
表示 UDP 數據報套接字)。protocol
:通常為 0,表示自動選擇對應協議(如 TCP 對應IPPROTO_TCP
)。- 返回值:成功返回套接字描述符(非負整數),失敗返回
-1
并設置errno
。
bind()? ? 綁定地址和端口 :
? ? ? ? ? ?
- 參數:
sockfd
:socket()
返回的套接字描述符。addr
:指向地址結構的指針(如struct sockaddr_in
)。addrlen
:地址結構的長度(如sizeof(struct sockaddr_in)
)。- 返回值:成功返回
0
,失敗返回-1
。
?listen()? ? ?監聽連接:
? ? ? ? ? ??
- 參數:
sockfd
:已綁定的套接字描述符。backlog
:未處理連接隊列的最大長度(如5
或SOMAXCONN
)。- 返回值:成功返回
0
,失敗返回-1
。- 說明:將套接字從主動模式轉為被動模式,等待客戶端連接。
?accept()? ?接受客戶端連接:
? ? ??
- 參數:
sockfd
:監聽套接字描述符(由listen()
創建)。addr
:存儲客戶端地址的結構體指針(可為NULL
)。addrlen
:地址結構體長度的指針(需初始化為結構體大小)。- 返回值:成功返回新的客戶端套接字描述符,失敗返回
-1
。- 說明:
- 阻塞直到有客戶端連接到達。
- 返回的新套接字用于與客戶端通信,原監聽套接字繼續監聽
?connect()? ? 連接服務器:
? ? ? ?
- 參數:
sockfd
:客戶端套接字描述符(由socket()
創建)。addr
:服務器地址結構體指針。addrlen
:地址結構體長度。- 返回值:成功返回
0
,失敗返回-1
。- 說明:
- 客戶端調用此函數發起與服務器的連接(觸發三次握手)。
- 若連接失敗(如服務器未監聽),需重新調用
connect()
。
?send()? ? 發送數據:
? ? ? ? ?
- 參數:
sockfd
:已連接的套接字描述符。buf
:待發送數據的緩沖區指針。len
:數據長度(字節)。flags
:通常為0
,或設置特殊標志(如MSG_DONTWAIT
表示非阻塞)。- 返回值:成功返回實際發送的字節數,失敗返回
-1
。- 說明:
- 數據可能未立即發送,而是存入發送緩沖區。
- 返回值可能小于
len
(如網絡擁塞),需循環發送剩余數據。
?
recv()? ? 接收數據 :
? ? ? ? ??
- 參數:
sockfd
:已連接的套接字描述符。buf
:存儲接收數據的緩沖區指針。len
:緩沖區最大長度。flags
:通常為0
,或設置特殊標志(如MSG_PEEK
表示查看但不取出數據)。- 返回值:
- 成功返回實際接收的字節數。
- 返回
0
表示對方已關閉連接(FIN 包)。- 返回
-1
表示出錯(如連接斷開)。- 說明:
- 若無數據且未關閉連接,
recv()
默認阻塞。- 需循環讀取直至數據全部接收(尤其對于大文件)。
?close()? ? 關閉套接字:
? ? ? ? ? ? ? ? ??
- 參數:
fd
:套接字描述符。- 返回值:成功返回
0
,失敗返回-1
。- 說明:
- 關閉套接字并釋放資源。
- 觸發 TCP 四次揮手斷開連接(若為主動關閉方)。
七、代碼實現?
- 服務端
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
int main(int argc, char** argv)
{//監聽套接字int listfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listfd){perror("socket");return 1;}// man 7 ipstruct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);ser.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = bind(listfd, (SA)&ser, sizeof(ser));if (-1 == ret){perror("bind");return 1;}// 三次握手的排隊數 ,listen(listfd, 3);socklen_t len = sizeof(cli);//通信套接字int conn = accept(listfd, (SA)&cli, &len);if (-1 == conn){perror("accept");return 1;}while (1){char buf[256] = {0};// ret >0 實際收到的字節數//==0 表示對方斷開// -1 出錯。ret = recv(conn, buf, sizeof(buf), 0);if(ret<=0){break;}printf("cli:%s\n",buf);time_t tm;time(&tm);struct tm * info = localtime(&tm);sprintf(buf,"%s %d:%d:%d\n",buf, info->tm_hour,info->tm_min,info->tm_sec);send(conn,buf,strlen(buf),0);}close(conn);close(listfd);// system("pause");return 0;
}
- 客戶端?
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
int main(int argc, char** argv)
{int conn = socket(AF_INET, SOCK_STREAM, 0);if (-1 == conn){perror("socket");return 1;}// man 7 ipstruct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);ser.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = connect(conn,(SA)&ser,sizeof(ser));if(-1 == ret){perror("connect");return 1;}while (1){char buf[256] = {0};strcpy(buf,"this is tcp test");send(conn,buf,strlen(buf),0);ret = recv(conn, buf, sizeof(buf), 0);if(ret<=0){break;}printf("ser:%s",buf);fflush(stdout);sleep(1);}close(conn);// system("pause");return 0;
}
八、黏包問題?
1.什么是粘包問題?
答:粘包問題是指在TCP通信中,發送方發送的多個獨立消息在接收方被合并成一個消息接收的現象。換句話說,發送方發送的多條消息在接收方被“粘”在一起,導致接收方無法直接區分消息的邊界。
2.粘包問題成因?
- TCP是面向流的協議,它將數據視為一個連續的字節流,不保留消息的邊界。
- 發送方發送的多個消息可能被合并到同一個TCP包中發送。
- 接收方在讀取數據時,無法直接知道哪些字節屬于哪條消息。
3.粘包問題的影響?
- 接收方無法正確解析消息,可能導致數據解析錯誤。
- 系統的健壯性和可靠性降低,尤其是在需要嚴格消息邊界的應用中。
4.如何解決?
- 添加分隔符:在每條消息末尾添加特殊分隔符(如
\n
或\r\n
),接收方通過分隔符來解析消息。 - 固定大小:每條消息的長度固定,接收方根據固定長度來解析消息。
- 自定義協議
九、練習
客戶端-服務端? 傳送文件
- 服務端
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
typedef struct
{char filename[256];char buf[1024];int buf_len;int total_len;
} PACK;
int main(int argc, char** argv)
{//監聽套接字int listfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listfd){perror("socket");return 1;}// man 7 ipstruct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);ser.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = bind(listfd, (SA)&ser, sizeof(ser));if (-1 == ret){perror("bind");return 1;}// 三次握手的排隊數 ,listen(listfd, 3);socklen_t len = sizeof(cli);//通信套接字int conn = accept(listfd, (SA)&cli, &len);if (-1 == conn){perror("accept");return 1;}int first_flag = 0;int fd = -1;int current_size = 0;int total_size = 0;while (1){PACK pack;bzero(&pack, sizeof(pack));ret = recv(conn, &pack, sizeof(pack), 0);if (0 == first_flag){first_flag = 1;fd = open(pack.filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);if (-1 == fd){perror("open");return 1;}total_size = pack.total_len;}if (0 == pack.buf_len){break;}write(fd, pack.buf, pack.buf_len);current_size += pack.buf_len;printf("%d/%d\n", current_size, total_size);bzero(&pack, sizeof(pack));strcpy(pack.buf,"go on");// send(conn,&pack,sizeof(pack),0);}close(conn);close(listfd);close(fd);// system("pause");return 0;
}
- 客戶端
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h> /* See NOTES */
#include <sys/types.h>
#include <time.h>
#include <unistd.h>typedef struct sockaddr*(SA);
typedef struct
{char filename[256];char buf[1024];int buf_len;int total_len;
} PACK;
int main(int argc, char** argv)
{int conn = socket(AF_INET, SOCK_STREAM, 0);if (-1 == conn){perror("socket");return 1;}// man 7 ipstruct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);ser.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = connect(conn, (SA)&ser, sizeof(ser));if (-1 == ret){perror("connect");return 1;}PACK pack;strcpy(pack.filename, "1.png"); // /home/linux/1.pngstruct stat st;ret = stat("/home/linux/1.png", &st);if (-1 == ret){perror("stat");return 1;}pack.total_len = st.st_size; // total sizeint fd = open("/home/linux/1.png", O_RDONLY);if (-1 == fd){perror("open");return 1;}while (1){pack.buf_len = read(fd, pack.buf, sizeof(pack.buf));send(conn, &pack, sizeof(pack), 0);if (pack.buf_len <= 0){break;}bzero(&pack,sizeof(pack));//recv(conn,&pack,sizeof(pack),0);usleep(1000*10); //10ms}close(conn);close(fd);// system("pause");return 0;
}
?