TCP網(wǎng)絡(luò)通訊
TCP編程流程
接口介紹
-
socket()方法是用來(lái)創(chuàng)建一個(gè)套接字,有了套接字就可以通過(guò)網(wǎng)絡(luò)進(jìn)行數(shù)據(jù)的收發(fā)。創(chuàng)建套接字時(shí)要指定使用的服務(wù)類型,使用 TCP 協(xié)議選擇流式服務(wù)(SOCK_STREAM)。
-
**bind()方法是用來(lái)指定套接字使用的 IP 地址和端口。**IP 地址就是自己主機(jī)的地址,測(cè)試程序時(shí)可以使用回環(huán)地址“127.0.0.1”。端口是一個(gè) 16 位的整形值,一般 0-1024 為知名端口,如 HTTP 使用的 80 號(hào)端口。這類端口一般用戶不能隨便使用。其次,1024-4096 為保留端口,用戶一般也不使用。4096 以上為臨時(shí)端口,用戶可以使用。在Linux 上,1024 以內(nèi)的端口號(hào),只有 root 用戶可以使用。
-
**listen()方法是用來(lái)創(chuàng)建監(jiān)聽隊(duì)列。**監(jiān)聽隊(duì)列有兩種,一個(gè)是存放未完成三次握手的連接,一種是存放已完成三次握手的連接。listen()第二個(gè)參數(shù)就是指定已完成三次握手隊(duì)列的長(zhǎng)度。
-
accept()處理存放在 listen 創(chuàng)建的已完成三次握手的隊(duì)列中的連接。每處理一個(gè)連接,則accept()返回該連接對(duì)應(yīng)的套接字描述符。如果該隊(duì)列為空,則 accept 阻塞。
-
connect()方法一般由客戶端程序執(zhí)行,需要指定連接的服務(wù)器端的 IP 地址和端口。該方法執(zhí)行后,會(huì)進(jìn)行三次握手, 建立連接。
-
send()方法用來(lái)向 TCP 連接的對(duì)端發(fā)送數(shù)據(jù)。send()執(zhí)行成功,只能說(shuō)明將數(shù)據(jù)成功寫入到發(fā)送端的發(fā)送緩沖區(qū)中,并不能說(shuō)明數(shù)據(jù)已經(jīng)發(fā)送到了對(duì)端。send()的返回值為實(shí)際寫入到發(fā)送緩沖區(qū)中的數(shù)據(jù)長(zhǎng)度。
-
recv()方法用來(lái)接收 TCP 連接的對(duì)端發(fā)送來(lái)的數(shù)據(jù)。recv()從本端的接收緩沖區(qū)中讀取數(shù)據(jù),如果接收緩沖區(qū)中沒有數(shù)據(jù),則 recv()方法會(huì)阻塞;返回值是實(shí)際讀到的字節(jié)數(shù),如果recv()返回值為 0, 說(shuō)明對(duì)方已經(jīng)關(guān)閉了 TCP 連接。
-
close()方法用來(lái)關(guān)閉 TCP 連接。此時(shí),會(huì)進(jìn)行四次揮手。
客戶端代碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
while (1)
{
char buff[128] = {0};
printf("input:\n");
fgets(buff, 128, stdin);
if (strncmp(buff, "end", 3) == 0)
{
break;
}
send(sockfd, buff, strlen(buff), 0);
memset(buff, 0, 128);
recv(sockfd, buff, 127, 0);
printf("buff=%s\n", buff);
}
close(sockfd);
exit(0);
}
服務(wù)端代碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000); // htons 將主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回環(huán)地址
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
res = listen(sockfd, 5);
if (-1 == res)
{
exit(1);
}
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int n = 0;
int c = -1;
while (1) // 服務(wù)器循環(huán)接收客戶端連接
{
char data[128] = {0};
if (n == 0)
{
c = accept(sockfd, (struct sockaddr *)&caddr, &len); // 阻塞
if (c == -1)
{
printf("accept error ");
continue;
;
}
}
n = recv(c, data, 127, 0); // 阻塞
if (n == 0) //連接關(guān)閉
{
close(c);
printf("client close\n");
continue;
}
else if (n < 0) //出錯(cuò)
{
printf("recv error");
continue;
}
printf("n = %d, buff = %s\n", n, data);
send(c, "OK", 2, 0);
}
close(sockfd);
exit(0);
}
運(yùn)行結(jié)果:
引入多線程處理并發(fā)
服務(wù)器端代碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *run(void *arg)
{
int c = (int)arg;
while (1)
{
char buff[128] = {0};
if (recv(c, buff, 127, 0) <= 0)
{
break;
}
printf("recv(%d)=%s", c, buff);
send(c, "ok", 2, 0);
}
printf("one client over(%d)\n", c);
close(c);
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr, caddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
listen(sockfd, 10);
while (1)
{
int len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (c < 0)
{
continue;
}
printf("accept c = %d\n", c);
pthread_t id;
pthread_create(&id, NULL, run, (void *)c);
}
close(sockfd);
exit(0);
}
運(yùn)行結(jié)果:
引入fork處理并發(fā)
服務(wù)器端代碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
void DealClientLink(int c, struct sockaddr_in caddr)
{
while (1)
{
char buff[128] = {0};
int n = recv(c, buff, 127, 0);
if (n <= 0)
{
break;
}
printf("%s:%d %s", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port), buff);
send(c, "OK", 2, 0);
}
printf("One Client Close\n");
close(c);
}
void Signal_Fun(int sign)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD, Signal_Fun); // 用wait()處理僵死進(jìn)程
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
printf("create sockfd error\n");
exit(1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
assert(-1 != res);
listen(sockfd, 10);
while (1)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
assert(-1 != c);
printf("%s:%d Link Success\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
pid_t pid = fork();
if (-1 == pid)
{
exit(1);
}
if (0 == pid)
{
DealClientLink(c,caddr);
exit(0); //必須結(jié)束子進(jìn)程,否則會(huì)有多個(gè)進(jìn)程調(diào) accept
}
else
{
close(c); //父子進(jìn)程都需要關(guān)閉 c
}
}
close(sockfd);
exit(0);
}
運(yùn)行結(jié)果:
TCP連接狀態(tài)轉(zhuǎn)變圖
三次握手
- 流程圖
-
使用netstat工具查看狀態(tài)變化(參考圖3-8)
四次揮手
-
流程圖
-
使用netstat命令查看狀態(tài)(參考圖3-8)
TIME_WAIT的作用
在圖3-8中,當(dāng)客戶端連接在收到服務(wù)器的結(jié)束報(bào)文段之后,并沒有直接進(jìn)人CLOSED 狀態(tài),而是轉(zhuǎn)移到 TIME_WAIT 狀態(tài)。在這個(gè)狀態(tài),客戶端連接要等待段長(zhǎng)為2MSL(Maximum Segment Life,報(bào)文段最大生存時(shí)間)的時(shí)間,才能完全關(guān)閉;MSL是 TCP 報(bào)文段在網(wǎng)絡(luò)中的最大生存時(shí)間,標(biāo)準(zhǔn)文檔 RFC 1122 的建議值是2 min;
TIME WAIT 狀態(tài)存在的原因有兩點(diǎn):
-
可靠地終止TCP 連接
當(dāng)服務(wù)器發(fā)給客戶端的ACK中途丟失,客戶端收不到ACK,會(huì)重新發(fā)送FIN,如果此時(shí)服務(wù)器已經(jīng)關(guān)閉,無(wú)法接收來(lái)自客戶端的FIN,便會(huì)陷入一種“藕斷絲連”狀態(tài)(一方關(guān)閉,一方未關(guān)閉)。這顯然是不合適的,因?yàn)門CP 連接是全雙工的,雙方完成數(shù)據(jù)交換之后,通信雙方都必須斷開連接以釋放系統(tǒng)資源。
-
保證讓遲來(lái)的TCP 報(bào)文段有足夠的時(shí)間被識(shí)別并丟棄
在 Linux 系統(tǒng)上,一個(gè)TCP 端口不能被同時(shí)打開多次(兩次及以上)。當(dāng)一個(gè)TCP 連接處于 TIME_WAIT 狀態(tài)時(shí),我們將無(wú)法立即使用該連接占用著的端口來(lái)建立一個(gè)新連接。反過(guò)來(lái),如果不存在 TIME WAIT 態(tài),則應(yīng)用序能夠立即建立一個(gè)和剛關(guān)閉的連接相似的連接(這里說(shuō)的相似,是指它們具有相同的 IP 地址和端口號(hào))。這個(gè)新的、和原來(lái)相似的連接被稱為原來(lái)的連接的化身 (incarmation)。新的化身可能接收到屬于原來(lái)的連接的、攜帶應(yīng)用程序數(shù)據(jù)的 TCP 報(bào)文段(遲到的報(bào)文段),這顯然是不應(yīng)該發(fā)生的。這就是 TIMEWAIT 狀態(tài)存在的第二個(gè)原因。
TCP協(xié)議特點(diǎn)
流式服務(wù)
TCP 字節(jié)流的特點(diǎn),發(fā)送端執(zhí)行的寫操作次數(shù)和接收端執(zhí)行的讀操作次數(shù)之間沒有任何數(shù)量關(guān)系,應(yīng)用程序?qū)?shù)據(jù)的發(fā)送和接收是沒有邊界限制的。如下圖:
TCP連接的可靠性
- IPV4報(bào)文格式:
- TCP報(bào)文格式:
- 應(yīng)答機(jī)制
- 超時(shí)重傳
TCP 傳輸是可靠的。首先,TCP 協(xié)議采用發(fā)送應(yīng)答機(jī)制,即發(fā)送端發(fā)送的每個(gè) TCP 報(bào)文段都必須得到接收方的應(yīng)答,才認(rèn)為這個(gè) TCP 報(bào)文段傳輸成功。其次,TCP 協(xié)議采用超時(shí)重傳機(jī)制,發(fā)送端在發(fā)送出1個(gè) TCP 報(bào)文段之后啟動(dòng)定時(shí)器,如果在定時(shí)時(shí)間內(nèi)未收到應(yīng)答,它將重發(fā)該報(bào)文段。最后,因?yàn)?TCP 報(bào)文段最終是以 IP數(shù)據(jù)報(bào)發(fā)送的,而 數(shù)據(jù)報(bào)到達(dá)接收端可能亂序、重復(fù),所以 TCP 協(xié)議還會(huì)對(duì)接收到的 TCP 報(bào)文段重排、整理,再交付給應(yīng)用層。
粘包問(wèn)題
在流式服務(wù)中如上圖3-9所示,盡管報(bào)文已經(jīng)按順序整理好并接受,但是無(wú)法分割成正確的信息,就形成了所謂的粘包問(wèn)題,為了解決此問(wèn)題,我們可以每次發(fā)送時(shí)進(jìn)行標(biāo)記分割,以便于接收方進(jìn)行分析和拆分,如下圖:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-827278.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-827278.html
到了這里,關(guān)于【Linux Day15 TCP網(wǎng)絡(luò)通訊】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!