Linux系統(tǒng)應(yīng)用編程(五)Linux網(wǎng)絡(luò)編程(上篇)
一、網(wǎng)絡(luò)基礎(chǔ)
1.兩個網(wǎng)絡(luò)模型和常見協(xié)議
(1)OSI七層模型(物數(shù)網(wǎng)傳會表應(yīng))
- 物理層、數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層、會話層、表示層、應(yīng)用層(自下到上)
(2)TCP/IP四層模型(網(wǎng)網(wǎng)傳應(yīng))
- 網(wǎng)絡(luò)接口層(鏈路層)、網(wǎng)絡(luò)層、傳輸層、應(yīng)用層
(3)常見網(wǎng)絡(luò)協(xié)議所屬層
2.字節(jié)序
(1)兩種字節(jié)序
(2)字節(jié)序轉(zhuǎn)換函數(shù)
3.TCP通信時序(三次握手、四次揮手)
以下均為簡述,僅針對面試時能夠有東西掰扯
(1)什么是"三次握手"和"四次揮手"
- "三次握手"意思是TCP客戶端和服務(wù)器建立連接需要3次通信的過程;
- "四次揮手"意思是TCP客戶端和服務(wù)器斷開連接需要4次通信的過程。
(2)"三次握手"和"四次揮手"的過程
-
“三次握手”:客戶端主動向服務(wù)器發(fā)起連接請求,也就是發(fā)送建立連接的標志位SYN,服務(wù)器收到該請求同意后回復(fù)一個SYN和ACK(應(yīng)答標志位),表示服務(wù)器收到客戶端的連接請求,客戶端收到服務(wù)器SYN+ACK后,再向服務(wù)器發(fā)送ACK應(yīng)答標志位,等到服務(wù)器收到后就完成了三次握手建立連接。
-
“四次揮手”:一般由客戶端主動斷開,發(fā)送FIN標志位給服務(wù)器后,客戶端處于半關(guān)閉狀態(tài)(也就是只能接收服務(wù)器數(shù)據(jù),而不能發(fā)送數(shù)據(jù));服務(wù)器接收到FIN后回復(fù)客戶端ACK應(yīng)答;接著服務(wù)器也會發(fā)送FIN給客戶端,同時服務(wù)器也進入半關(guān)閉狀態(tài),直到客戶端回復(fù)ACK給到服務(wù)器,連接斷開。
實際上,套接字在內(nèi)核中實現(xiàn)了讀、寫兩個緩沖區(qū),半關(guān)閉就是關(guān)閉了寫緩沖區(qū)
-
【補充】上面說到客戶端處于半關(guān)閉,為什么可以在第四揮手時給服務(wù)器回復(fù)ACK?
半關(guān)閉只是關(guān)閉socket中的寫緩沖區(qū),此時客戶端和服務(wù)器的socket連接并沒有關(guān)閉,因此,在半關(guān)閉狀態(tài)下,客戶端仍然可以通過已經(jīng)建立好的TCP連接給服務(wù)器回復(fù)ACK確認包來完成四次揮手的過程。
(3)為什么斷開連接需要"四次揮手"
- 導(dǎo)致TCP連接關(guān)閉需要四次揮手的直接原因:半關(guān)閉
- 為什么:為了確保雙方在關(guān)閉連接之前都能夠完成必要的操作,并盡可能地減少因網(wǎng)絡(luò)不穩(wěn)定性造成的影響,以保證數(shù)據(jù)的可靠性。
二、Socket網(wǎng)絡(luò)編程
1.網(wǎng)絡(luò)地址結(jié)構(gòu)體
2.Socket編程API
(1)創(chuàng)建套接字socket( )
(2)綁定地址bind( )
(3)設(shè)置監(jiān)聽listen( )
(4)等待連接accept( )
(5)發(fā)起連接connect( )
(6)設(shè)置地址復(fù)用setsockopt( )
三、案例程序
本案例參考于抖音up@小飛有點東西《python全棧高級篇》,up的python視頻很nb;以下為筆者學(xué)習后用C語言描述的版本
1.簡易"模擬Linux終端"v1.0
【開發(fā)環(huán)境】 ubuntu22.04、CLion
【核心技術(shù)】 TCP網(wǎng)絡(luò)編程、服務(wù)器多進程/多線程并發(fā)、解決粘包問題
【案例描述】 client接入server后,通過命令行輸入Linux命令,由server執(zhí)行后的結(jié)果發(fā)送給client。
【v1.0代碼】 多進程實現(xiàn)服務(wù)器并發(fā),父進程回收子進程避免僵尸進程,子進程和客戶端通信。
至此,程序還有BUG未解決——粘包問題
#include "temp.h" //many head files in it
/* 服務(wù)器socket結(jié)構(gòu)體 */
struct ServerSocket{
int sockfd; //服務(wù)器socket文件描述符
void (* socketBind)(int ,char *,int); //給sockfd綁定地址函數(shù)
void (* serverListen)(int , int); //監(jiān)聽sockfd函數(shù)
struct ClientSocket (* serverAccept)(int); //建立連接函數(shù)
};
/* 客戶端socket結(jié)構(gòu)體 */
struct ClientSocket{
int cfd; //建立連接的socket文件描述符
char ip[32]; //客戶端IP
int port; //客戶端Port
};
/* 服務(wù)器socket綁定地址信息函數(shù)實現(xiàn) */
void socketBind(int sockfd,char *ip,int port){
int retn;
/* 初始化地址結(jié)構(gòu)體sockaddr_in */
struct sockaddr_in serAddr = {
.sin_port = htons(port),
.sin_family = AF_INET
};
inet_pton(AF_INET,ip,&serAddr.sin_addr.s_addr);
/* 調(diào)用bind()綁定地址 */
retn = bind(sockfd,(struct sockaddr *)&serAddr,sizeof(serAddr));
if(retn == -1){
perror("bind");
exit(-1);
}
printf("<Server> bind address: %s:%d\n",ip,port);
}
/* 服務(wù)器socket監(jiān)聽函數(shù)實現(xiàn) */
void serverListen(int sockfd,int n){
int retn;
retn = listen(sockfd,n);
if(retn == -1){
perror("listen");
exit(-1);
}
printf("<Server> listening...\n");
}
/* 服務(wù)器建立連接函數(shù)實現(xiàn),返回值為struct ClientSocket結(jié)構(gòu)體 *
* (包括建立連接的socket文件描述符、客戶端信息) */
struct ClientSocket serverAccept(int sockfd){
struct sockaddr_in clientAddr;
socklen_t addrLen = sizeof(clientAddr);
struct ClientSocket c_socket;
c_socket.cfd = accept(sockfd,(struct sockaddr *)&clientAddr,&addrLen);
if(c_socket.cfd == -1){
perror("accept");
exit(-1);
}else{
c_socket.port = ntohs(clientAddr.sin_port);
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,c_socket.ip,sizeof(clientAddr));
return c_socket;
}
}
/* 信號處理函數(shù):回收子進程 */
void waitChild(int signum){
wait(NULL);
}
int main(){
/* 初始化服務(wù)器socket */
struct ServerSocket ss = {
.serverAccept = serverAccept,
.socketBind = socketBind,
.serverListen = serverListen
};
/* 設(shè)置端口復(fù)用 */
int optval = 1;
setsockopt(ss.sockfd,SOL_SOCKET,SO_REUSEPORT,&optval,sizeof(optval));
ss.sockfd = socket(AF_INET,SOCK_STREAM,0);
ss.socketBind(ss.sockfd,"192.168.35.128",8880);
ss.serverListen(ss.sockfd,128);
/* 多進程實現(xiàn)服務(wù)器并發(fā) */
struct ClientSocket cs; //客戶端socket
pid_t pid = 1;
int nread;
while(1){ //循環(huán)等待客戶端接入
cs = ss.serverAccept(ss.sockfd);
printf("<Server> client connected.(%s:%d)\n",cs.ip,cs.port);
pid = fork(); //創(chuàng)建父子進程
if(pid > 0){ //父進程
close(cs.cfd); //關(guān)閉通信的套接字
signal(SIGCHLD,waitChild); //注冊信號
continue;
}else if(pid == 0){ //子進程
close(ss.sockfd); //關(guān)閉建立連接的socket
while(1){
char *writeBuff = (char *) malloc(2048); //寫buff
char *readBuff = (char *) malloc(128); //讀buff
FILE *buffFile = NULL; //文件流
while(1) {
nread = read(cs.cfd, readBuff, 128); //讀取客戶端發(fā)過來的命令
/* 對read判空,防止客戶端退出后一直收空數(shù)據(jù)的死循環(huán) */
if (nread == 0) {
printf("<server> client disconnected (%s:%d)\n",cs.ip,cs.port);
break;
}
/* 執(zhí)行客戶端發(fā)過來的命令 */
buffFile = popen(readBuff, "r");
fread(writeBuff, 2048, 1, buffFile); //命令執(zhí)行成功結(jié)果讀取到writeBuff
if (strlen(writeBuff) == 0) {
write(cs.cfd, "\n", 1);
}else{
write(cs.cfd, writeBuff, strlen(writeBuff)); //結(jié)果寫回給客戶端
}
/* 清空緩存數(shù)據(jù),關(guān)閉流 */
memset(writeBuff, '\0', strlen(writeBuff));
memset(readBuff, '\0', strlen(readBuff));
pclose(buffFile);
}
return 0;
}
}else{
perror("fork");
exit(-1);
}
}
}
2.TCP粘包問題
(1)粘包問題引入
- v1.0的服務(wù)器代碼,只執(zhí)行了ls、dir執(zhí)行結(jié)果較短的命令,看似沒有BUG,但是如果執(zhí)行的是像ps -aux命令結(jié)果較長的,就可以發(fā)現(xiàn),由于返回的結(jié)果較長,客戶端一次讀取并沒有讀取完(或者讀取太快、緩存太?。?,當下一條命令執(zhí)行后,結(jié)果就會和上一條命令沒有讀取完的內(nèi)容連在一起。如圖:
- 針對客戶端讀取數(shù)據(jù)太快,或客戶端設(shè)置的緩存太小,雖然我們在代碼中,用延時避免讀取數(shù)據(jù)太快、設(shè)置較大的緩存區(qū)可以一定程度避免粘包問題,但是這種解決方法并不好,延時難免影響用戶體驗,過大的緩存區(qū)也不切實際。所以,需要從其他角度解決TCP的粘包問題。
(2)TCP粘包產(chǎn)生原因
- TCP協(xié)議基于字節(jié)流傳輸數(shù)據(jù),并不是基于消息,數(shù)據(jù)類似水流傳輸著,數(shù)據(jù)之間難以區(qū)分,所以不可避免出現(xiàn)將多個獨立的數(shù)據(jù)包粘成一個數(shù)據(jù)包的情況;
- TCP為了避免網(wǎng)絡(luò)擁塞,減少網(wǎng)絡(luò)負載而設(shè)計的底層優(yōu)化算法Nagle算法,通過將多個小數(shù)據(jù)包合并成一個大數(shù)據(jù)包進行發(fā)送,以減少網(wǎng)絡(luò)流量和傳輸延遲。當有大量小數(shù)據(jù)包需要發(fā)送時,Nagle算法會將這些數(shù)據(jù)包先緩存起來,并在緩存區(qū)中嘗試組裝成一個更大的數(shù)據(jù)包再進行發(fā)送。所以如果接收方不能及時地處理接收到的數(shù)據(jù)包,或者發(fā)送方的緩存區(qū)未被填滿,那么就會導(dǎo)致TCP粘包問題的產(chǎn)生。
(3)解決粘包問題
- 固定數(shù)據(jù)包的長度:每次發(fā)送讀取都固定大小
-
在數(shù)據(jù)頭部加入數(shù)據(jù)的總長度:接收方先讀取消息頭中的長度信息,再根據(jù)長度信息讀取對應(yīng)長度的數(shù)據(jù)
(實際上也就是<自定義協(xié)議>)
- 特殊分割符:使用特殊的分割符(如\n或者\r\n)來分割每條數(shù)據(jù)
(4)自定義協(xié)議
-
自定義協(xié)議通常包含兩部分內(nèi)容:
-
消息頭:用于描述數(shù)據(jù)包的基本信息,如數(shù)據(jù)包類型、數(shù)據(jù)包長度等。
例如:<文件傳輸>頭部可以包括文件類型、文件的md5值、文件的大小等
-
消息體:用于存儲具體的數(shù)據(jù),如文本、圖片、音頻等。
-
-
設(shè)計自定義協(xié)議時,需要遵循以下幾個原則:
- 協(xié)議必須是可擴展的,能夠容易地添加新的消息類型或字段。
- 消息的格式必須明確并符合規(guī)范,可以使用固定長度、分隔符、標記等方式來辨別消息的開始和結(jié)束。
- 在消息頭中要包含足夠的元信息,能夠讓接收方對消息進行正確的處理。
- 協(xié)議設(shè)計必須考慮網(wǎng)絡(luò)上的安全問題,避免數(shù)據(jù)泄露和信息篡改等風險。
-
自定義協(xié)議通常用于特定領(lǐng)域的應(yīng)用,如游戲開發(fā)、嵌入式系統(tǒng)、金融交易等場景。自定義協(xié)議的設(shè)計和實現(xiàn)需要結(jié)合具體場景進行考慮,需要對網(wǎng)絡(luò)協(xié)議有一定的了解,并且需要注意協(xié)議的可靠性、可擴展性和安全性等問題。
3.簡易"模擬Linux終端"v2.0
【Server v2.0】 通過在數(shù)據(jù)頭部加入數(shù)據(jù)的總長度,客戶端先讀取數(shù)據(jù)的總長度,決定本次讀取的大小,解決粘包問題
#include "temp.h" //many head files in it
/* 服務(wù)器socket結(jié)構(gòu)體 */
struct ServerSocket{
int sockfd; //服務(wù)器socket文件描述符
void (* socketBind)(int ,char *,int); //給sockfd綁定地址函數(shù)
void (* serverListen)(int , int); //監(jiān)聽sockfd函數(shù)
struct ClientSocket (* serverAccept)(int); //建立連接函數(shù)
};
/* 客戶端socket結(jié)構(gòu)體 */
struct ClientSocket{
int cfd; //建立連接的socket文件描述符
char ip[32]; //客戶端IP
int port; //客戶端Port
};
/* 數(shù)據(jù)結(jié)構(gòu)體 */
struct Data{
int headerLenth; //數(shù)據(jù)頭部長度
long dataLenth; //數(shù)據(jù)長度(命令執(zhí)行成功的結(jié)果長度)
char *dataBody; //數(shù)據(jù)正文(命令執(zhí)行成功的結(jié)果)
};
/* 服務(wù)器socket綁定地址信息函數(shù)實現(xiàn) */
void socketBind(int sockfd,char *ip,int port){
int retn;
/* 初始化地址結(jié)構(gòu)體sockaddr_in */
struct sockaddr_in serAddr = {
.sin_port = htons(port),
.sin_family = AF_INET
};
inet_pton(AF_INET,ip,&serAddr.sin_addr.s_addr);
/* 調(diào)用bind()綁定地址 */
retn = bind(sockfd,(struct sockaddr *)&serAddr,sizeof(serAddr));
if(retn == -1){
perror("bind");
exit(-1);
}
printf("<Server> bind address: %s:%d\n",ip,port);
}
/* 服務(wù)器socket監(jiān)聽函數(shù)實現(xiàn) */
void serverListen(int sockfd,int n){
int retn;
retn = listen(sockfd,n);
if(retn == -1){
perror("listen");
exit(-1);
}
printf("<Server> listening...\n");
}
/* 服務(wù)器建立連接函數(shù)實現(xiàn),返回值為struct ClientSocket結(jié)構(gòu)體 *
* (包括建立連接的socket文件描述符、客戶端信息) */
struct ClientSocket serverAccept(int sockfd){
struct sockaddr_in clientAddr;
socklen_t addrLen = sizeof(clientAddr);
struct ClientSocket c_socket;
c_socket.cfd = accept(sockfd,(struct sockaddr *)&clientAddr,&addrLen);
if(c_socket.cfd == -1){
perror("accept");
exit(-1);
}else{
c_socket.port = ntohs(clientAddr.sin_port);
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,c_socket.ip,sizeof(clientAddr));
return c_socket;
}
}
/* 信號處理函數(shù):回收子進程 */
void waitChild(int signum){
wait(NULL);
}
/* 處理數(shù)據(jù)的函數(shù),返回值為struct Data */
struct Data dataDealWith(FILE *file){
char *tempBuff = (char *)malloc(8192); //臨時buff
long readBytes = 0; //讀取的字節(jié)數(shù)
struct Data data = {
.dataLenth = 0,
.dataBody = NULL
};
/* 處理數(shù)據(jù):計算數(shù)據(jù)正文大小,并保留管道中的數(shù)據(jù)到data.dataBody(需要動態(tài)調(diào)整大小) */
while(fread(tempBuff,sizeof(char),8192,file) > 0){
readBytes = strlen(tempBuff)+1; //讀到臨時buff的字節(jié)數(shù)
data.dataLenth += readBytes; //數(shù)據(jù)長度累加readBytes
if(data.dataLenth <= readBytes){ //如果數(shù)據(jù)長度小于設(shè)置的tempBuff大小,直接拷貝
data.dataBody = (char *)malloc(readBytes);
strcpy(data.dataBody,tempBuff);
}else if(data.dataLenth > readBytes){ //如果數(shù)據(jù)長度大于設(shè)置的tempBuff大小,擴容后拼接到后面
data.dataBody = realloc(data.dataBody,data.dataLenth);
strcat(data.dataBody,tempBuff);
}
data.dataBody[strlen(data.dataBody)+1] = '\0';
memset(tempBuff,'\0',8192);
}
free(tempBuff); //釋放臨時buff
return data;
}
int main(){
/* 初始化服務(wù)器socket */
struct ServerSocket ss = {
.serverAccept = serverAccept,
.socketBind = socketBind,
.serverListen = serverListen
};
/* 設(shè)置端口復(fù)用 */
int optval = 1;
setsockopt(ss.sockfd,SOL_SOCKET,SO_REUSEPORT,&optval,sizeof(optval));
ss.sockfd = socket(AF_INET,SOCK_STREAM,0);
ss.socketBind(ss.sockfd,"192.168.35.128",8880);
ss.serverListen(ss.sockfd,128);
/* 多進程實現(xiàn)服務(wù)器并發(fā) */
struct ClientSocket cs; //客戶端socket
pid_t pid = 1;
int nread;
while(1){ //循環(huán)等待客戶端接入
cs = ss.serverAccept(ss.sockfd);
printf("<Server> client connected.(%s:%d)\n",cs.ip,cs.port);
pid = fork(); //創(chuàng)建父子進程
if(pid > 0){ //父進程
close(cs.cfd); //關(guān)閉通信的套接字
signal(SIGCHLD,waitChild); //注冊信號
continue;
}else if(pid == 0){ //子進程
close(ss.sockfd); //關(guān)閉建立連接的socket
while(1){
char *readBuff = (char *) malloc(128); //讀buff
FILE *buffFile = NULL; //文件流
struct Data data;
char head[8];
while(1) {
nread = read(cs.cfd, readBuff, 128); //讀取客戶端發(fā)過來的命令
/* 對read判空,防止客戶端退出后一直收空數(shù)據(jù)的死循環(huán) */
if (nread == 0) {
printf("<server> client disconnected (%s:%d)\n",cs.ip,cs.port);
break;
}
/* 執(zhí)行客戶端發(fā)過來的命令 */
buffFile = popen(readBuff, "r"); //命令執(zhí)行成功結(jié)果讀取到writeBuff
data = dataDealWith(buffFile);
sprintf(head,"%ld",data.dataLenth);
write(cs.cfd,head, 8);
write(cs.cfd,data.dataBody,data.dataLenth);
memset(readBuff, '\0', strlen(readBuff));
memset(&data,0,sizeof(data));
pclose(buffFile);
}
exit(1);
}
}else{
perror("fork");
exit(-1);
}
}
}
【Client v2.0】文章來源:http://www.zghlxwxcb.cn/news/detail-424762.html
#include "temp.h"
int main(){
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in serAddr = {
.sin_family = AF_INET,
.sin_port = htons(8880)
};
inet_pton(AF_INET,"192.168.35.128",&serAddr.sin_addr.s_addr);
int retn = connect(fd,(struct sockaddr *)&serAddr,sizeof(serAddr) );
if(retn == -1){
perror("connect");
exit(-1);
}
char *writeBuff = (char *)malloc(128);
char *readBuff = (char *)malloc(1024);
char *header = (char *)malloc(8);
int nread = 0;
int dataLength = 0;
while(1){
printf("user@ubuntu-22.04:");
fgets(writeBuff,128,stdin);
if(*writeBuff == ' ' || *writeBuff == '\n'){
continue;
}
write(fd,writeBuff, strlen(writeBuff));
read(fd,header,8);
if(atol(header) == 0)continue;
printf("header:%ld\n", atol(header));
while(dataLength <= atol(header)){
read(fd,readBuff,1024);
dataLength += strlen(readBuff)+1;
printf("%s",readBuff);
memset(readBuff,'\0', 1024);
if(dataLength >= atol(header)){
dataLength = 0;
break;
}
}
memset(header,'\0', strlen(header));
memset(writeBuff,'\0', strlen(writeBuff));
printf("done\n");
}
}
文章來源地址http://www.zghlxwxcb.cn/news/detail-424762.html
到了這里,關(guān)于Linux系統(tǒng)應(yīng)用編程(五)Linux網(wǎng)絡(luò)編程(上篇)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!