本篇主要内容:
- Linux系统应用编程(五)Linux网络编程(上篇)
- 一、网络基础
- 1.两个网络模型和常见协议
- (1)OSI七层模型(物数网传会表应)
- (2)TCP/IP四层模型(网网传应)
- (3)常见网络协议所属层
- 2.字节序
- (1)两种字节序
- (2)字节序转换函数
- 3.TCP通信时序(三次握手、四次挥手)
- (1)什么是"三次握手"和"四次挥手"
- (2)"三次握手"和"四次挥手"的过程
- (3)为什么断开连接需要"四次挥手"
- 二、Socket网络编程
- 1.网络地址结构体
- 2.Socket编程API
- (1)创建套接字socket( )
- (2)绑定地址bind( )
- (3)设置监听listen( )
- (4)等待连接accept( )
- (5)发起连接connect( )
- (6)设置地址复用setsockopt( )
- 三、案例程序
- 1.简易"模拟Linux终端"v1.0
- 2.TCP粘包问题
- (1)粘包问题引入
- (2)TCP粘包产生原因
- (3)解决粘包问题
- (4)自定义协议
- 3.简易"模拟Linux终端"v2.0
Linux系统应用编程(五)Linux网络编程(上篇)
一、网络基础
1.两个网络模型和常见协议
(1)OSI七层模型(物数网传会表应)
- 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层(自下到上)
(2)TCP/IP四层模型(网网传应)
- 网络接口层(链路层)、网络层、传输层、应用层
(3)常见网络协议所属层
2.字节序
(1)两种字节序
(2)字节序转换函数
3.TCP通信时序(三次握手、四次挥手)
以下均为简述,仅针对面试时能够有东西掰扯
(1)什么是"三次握手"和"四次挥手"
- "三次握手"意思是TCP客户端和服务器建立连接需要3次通信的过程;
- "四次挥手"意思是TCP客户端和服务器断开连接需要4次通信的过程。
(2)"三次握手"和"四次挥手"的过程
-
“三次握手”:客户端主动向服务器发起连接请求,也就是发送建立连接的标志位SYN,服务器收到该请求同意后回复一个SYN和ACK(应答标志位),表示服务器收到客户端的连接请求,客户端收到服务器SYN+ACK后,再向服务器发送ACK应答标志位,等到服务器收到后就完成了三次握手建立连接。
-
“四次挥手”:一般由客户端主动断开,发送FIN标志位给服务器后,客户端处于半关闭状态(也就是只能接收服务器数据,而不能发送数据);服务器接收到FIN后回复客户端ACK应答;接着服务器也会发送FIN给客户端,同时服务器也进入半关闭状态,直到客户端回复ACK给到服务器,连接断开。
实际上,套接字在内核中实现了读、写两个缓冲区,半关闭就是关闭了写缓冲区
-
【补充】上面说到客户端处于半关闭,为什么可以在第四挥手时给服务器回复ACK?
半关闭只是关闭socket中的写缓冲区,此时客户端和服务器的socket连接并没有关闭,因此,在半关闭状态下,客户端仍然可以通过已经建立好的TCP连接给服务器回复ACK确认包来完成四次挥手的过程。
(3)为什么断开连接需要"四次挥手"
- 导致TCP连接关闭需要四次挥手的直接原因:半关闭
- 为什么:为了确保双方在关闭连接之前都能够完成必要的操作,并尽可能地减少因网络不稳定性造成的影响,以保证数据的可靠性。
二、Socket网络编程
1.网络地址结构体
2.Socket编程API
(1)创建套接字socket( )
(2)绑定地址bind( )
(3)设置监听listen( )
(4)等待连接accept( )
(5)发起连接connect( )
(6)设置地址复用setsockopt( )
三、案例程序
本案例参考于抖音up@小飞有点东西《python全栈高级篇》,up的python视频很nb;以下为笔者学习后用C语言描述的版本
1.简易"模拟Linux终端"v1.0
【开发环境】 ubuntu22.04、CLion
【核心技术】 TCP网络编程、服务器多进程/多线程并发、解决粘包问题
【案例描述】 client接入server后,通过命令行输入Linux命令,由server执行后的结果发送给client。
【v1.0代码】 多进程实现服务器并发,父进程回收子进程避免僵尸进程,子进程和客户端通信。
至此,程序还有BUG未解决——粘包问题
#include "temp.h" //many head files in it
/* 服务器socket结构体 */
struct ServerSocket{
int sockfd; //服务器socket文件描述符
void (* socketBind)(int ,char *,int); //给sockfd绑定地址函数
void (* serverListen)(int , int); //监听sockfd函数
struct ClientSocket (* serverAccept)(int); //建立连接函数
};
/* 客户端socket结构体 */
struct ClientSocket{
int cfd; //建立连接的socket文件描述符
char ip[32]; //客户端IP
int port; //客户端Port
};
/* 服务器socket绑定地址信息函数实现 */
void socketBind(int sockfd,char *ip,int port){
int retn;
/* 初始化地址结构体sockaddr_in */
struct sockaddr_in serAddr = {
.sin_port = htons(port),
.sin_family = AF_INET
};
inet_pton(AF_INET,ip,&serAddr.sin_addr.s_addr);
/* 调用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);
}
/* 服务器socket监听函数实现 */
void serverListen(int sockfd,int n){
int retn;
retn = listen(sockfd,n);
if(retn == -1){
perror("listen");
exit(-1);
}
printf("<Server> listening...\n");
}
/* 服务器建立连接函数实现,返回值为struct ClientSocket结构体 *
* (包括建立连接的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;
}
}
/* 信号处理函数:回收子进程 */
void waitChild(int signum){
wait(NULL);
}
int main(){
/* 初始化服务器socket */
struct ServerSocket ss = {
.serverAccept = serverAccept,
.socketBind = socketBind,
.serverListen = serverListen
};
/* 设置端口复用 */
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);
/* 多进程实现服务器并发 */
struct ClientSocket cs; //客户端socket
pid_t pid = 1;
int nread;
while(1){ //循环等待客户端接入
cs = ss.serverAccept(ss.sockfd);
printf("<Server> client connected.(%s:%d)\n",cs.ip,cs.port);
pid = fork(); //创建父子进程
if(pid > 0){ //父进程
close(cs.cfd); //关闭通信的套接字
signal(SIGCHLD,waitChild); //注册信号
continue;
}else if(pid == 0){ //子进程
close(ss.sockfd); //关闭建立连接的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); //读取客户端发过来的命令
/* 对read判空,防止客户端退出后一直收空数据的死循环 */
if (nread == 0) {
printf("<server> client disconnected (%s:%d)\n",cs.ip,cs.port);
break;
}
/* 执行客户端发过来的命令 */
buffFile = popen(readBuff, "r");
fread(writeBuff, 2048, 1, buffFile); //命令执行成功结果读取到writeBuff
if (strlen(writeBuff) == 0) {
write(cs.cfd, "\n", 1);
}else{
write(cs.cfd, writeBuff, strlen(writeBuff)); //结果写回给客户端
}
/* 清空缓存数据,关闭流 */
memset(writeBuff, '\0', strlen(writeBuff));
memset(readBuff, '\0', strlen(readBuff));
pclose(buffFile);
}
return 0;
}
}else{
perror("fork");
exit(-1);
}
}
}
2.TCP粘包问题
(1)粘包问题引入
- v1.0的服务器代码,只执行了ls、dir执行结果较短的命令,看似没有BUG,但是如果执行的是像ps -aux命令结果较长的,就可以发现,由于返回的结果较长,客户端一次读取并没有读取完(或者读取太快、缓存太小),当下一条命令执行后,结果就会和上一条命令没有读取完的内容连在一起。如图:
- 针对客户端读取数据太快,或客户端设置的缓存太小,虽然我们在代码中,用延时避免读取数据太快、设置较大的缓存区可以一定程度避免粘包问题,但是这种解决方法并不好,延时难免影响用户体验,过大的缓存区也不切实际。所以,需要从其他角度解决TCP的粘包问题。
(2)TCP粘包产生原因
- TCP协议基于字节流传输数据,并不是基于消息,数据类似水流传输着,数据之间难以区分,所以不可避免出现将多个独立的数据包粘成一个数据包的情况;
- TCP为了避免网络拥塞,减少网络负载而设计的底层优化算法Nagle算法,通过将多个小数据包合并成一个大数据包进行发送,以减少网络流量和传输延迟。当有大量小数据包需要发送时,Nagle算法会将这些数据包先缓存起来,并在缓存区中尝试组装成一个更大的数据包再进行发送。所以如果接收方不能及时地处理接收到的数据包,或者发送方的缓存区未被填满,那么就会导致TCP粘包问题的产生。
(3)解决粘包问题
- 固定数据包的长度:每次发送读取都固定大小
- 在数据头部加入数据的总长度:接收方先读取消息头中的长度信息,再根据长度信息读取对应长度的数据
(实际上也就是<自定义协议>)
- 特殊分割符:使用特殊的分割符(如\n或者\r\n)来分割每条数据
(4)自定义协议
-
自定义协议通常包含两部分内容:
-
消息头:用于描述数据包的基本信息,如数据包类型、数据包长度等。
例如:<文件传输>头部可以包括文件类型、文件的md5值、文件的大小等
-
消息体:用于存储具体的数据,如文本、图片、音频等。
-
-
设计自定义协议时,需要遵循以下几个原则:
- 协议必须是可扩展的,能够容易地添加新的消息类型或字段。
- 消息的格式必须明确并符合规范,可以使用固定长度、分隔符、标记等方式来辨别消息的开始和结束。
- 在消息头中要包含足够的元信息,能够让接收方对消息进行正确的处理。
- 协议设计必须考虑网络上的安全问题,避免数据泄露和信息篡改等风险。
-
自定义协议通常用于特定领域的应用,如游戏开发、嵌入式系统、金融交易等场景。自定义协议的设计和实现需要结合具体场景进行考虑,需要对网络协议有一定的了解,并且需要注意协议的可靠性、可扩展性和安全性等问题。
3.简易"模拟Linux终端"v2.0
【Server v2.0】 通过在数据头部加入数据的总长度,客户端先读取数据的总长度,决定本次读取的大小,解决粘包问题
#include "temp.h" //many head files in it
/* 服务器socket结构体 */
struct ServerSocket{
int sockfd; //服务器socket文件描述符
void (* socketBind)(int ,char *,int); //给sockfd绑定地址函数
void (* serverListen)(int , int); //监听sockfd函数
struct ClientSocket (* serverAccept)(int); //建立连接函数
};
/* 客户端socket结构体 */
struct ClientSocket{
int cfd; //建立连接的socket文件描述符
char ip[32]; //客户端IP
int port; //客户端Port
};
/* 数据结构体 */
struct Data{
int headerLenth; //数据头部长度
long dataLenth; //数据长度(命令执行成功的结果长度)
char *dataBody; //数据正文(命令执行成功的结果)
};
/* 服务器socket绑定地址信息函数实现 */
void socketBind(int sockfd,char *ip,int port){
int retn;
/* 初始化地址结构体sockaddr_in */
struct sockaddr_in serAddr = {
.sin_port = htons(port),
.sin_family = AF_INET
};
inet_pton(AF_INET,ip,&serAddr.sin_addr.s_addr);
/* 调用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);
}
/* 服务器socket监听函数实现 */
void serverListen(int sockfd,int n){
int retn;
retn = listen(sockfd,n);
if(retn == -1){
perror("listen");
exit(-1);
}
printf("<Server> listening...\n");
}
/* 服务器建立连接函数实现,返回值为struct ClientSocket结构体 *
* (包括建立连接的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;
}
}
/* 信号处理函数:回收子进程 */
void waitChild(int signum){
wait(NULL);
}
/* 处理数据的函数,返回值为struct Data */
struct Data dataDealWith(FILE *file){
char *tempBuff = (char *)malloc(8192); //临时buff
long readBytes = 0; //读取的字节数
struct Data data = {
.dataLenth = 0,
.dataBody = NULL
};
/* 处理数据:计算数据正文大小,并保留管道中的数据到data.dataBody(需要动态调整大小) */
while(fread(tempBuff,sizeof(char),8192,file) > 0){
readBytes = strlen(tempBuff)+1; //读到临时buff的字节数
data.dataLenth += readBytes; //数据长度累加readBytes
if(data.dataLenth <= readBytes){ //如果数据长度小于设置的tempBuff大小,直接拷贝
data.dataBody = (char *)malloc(readBytes);
strcpy(data.dataBody,tempBuff);
}else if(data.dataLenth > readBytes){ //如果数据长度大于设置的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(){
/* 初始化服务器socket */
struct ServerSocket ss = {
.serverAccept = serverAccept,
.socketBind = socketBind,
.serverListen = serverListen
};
/* 设置端口复用 */
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);
/* 多进程实现服务器并发 */
struct ClientSocket cs; //客户端socket
pid_t pid = 1;
int nread;
while(1){ //循环等待客户端接入
cs = ss.serverAccept(ss.sockfd);
printf("<Server> client connected.(%s:%d)\n",cs.ip,cs.port);
pid = fork(); //创建父子进程
if(pid > 0){ //父进程
close(cs.cfd); //关闭通信的套接字
signal(SIGCHLD,waitChild); //注册信号
continue;
}else if(pid == 0){ //子进程
close(ss.sockfd); //关闭建立连接的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); //读取客户端发过来的命令
/* 对read判空,防止客户端退出后一直收空数据的死循环 */
if (nread == 0) {
printf("<server> client disconnected (%s:%d)\n",cs.ip,cs.port);
break;
}
/* 执行客户端发过来的命令 */
buffFile = popen(readBuff, "r"); //命令执行成功结果读取到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】
#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");
}
}