文章目录
4. 优缺点
前言
记录学习嵌入式Linux网络编程的知识重点与难点,若涉及版权问题请联系本人删除!
一、网络知识概述
1. 网路结构分层
①主要存在两种分层模型:OSI七层模型(理论)和TCP/IP四层模型(实际)。
②分层思想:采用分治法,将复杂问题划分为各个层级上的子问题。每一层向上提供服务,同时使用下层提供的服务。
③网络封包和拆包:如下图所示,从主机A传输数据到主机B的流程一般为:主机A的应用层封装好了传输数据后,传输层会给上层数据包添加TCP头部,网络层会给上层数据包添加IP头部,网络接口层会给上层数据包添加对应的头部和尾部(一般为CRC校验);路由器将接收到的帧去掉头部和尾部传输给上一层,传输层将解析IP地址并查表转发,然后继续封装为帧,传输到指定的主机B;主机B逐层拆包,最终在应用层获取接收到的真正数据。
2. socket
①概念:socket是一个特殊的文件描述符,用于网络通信。
②socket分类:
- 流式套接字(SOCK_STREAM):对应TCP,面向连接、可靠。
- 数据报套接字(SOCK_DGRAM):对应UDP,无连接、不可靠。
- 原始套接字(SOCK_RAW):对应多个协议,可以直接访问IP、ICMP,跨过了传输层。
3. IP地址
①概念:IP地址是网络中主机地址的标识。通过IP可以找到对应的主机。
②IP地址分类:
- IPv4:一个32位(4字节)的整数,每个字节用.来分隔。每个字节的数据范围为0~255。例如: 192.168.5.11 不够用,引入局域网可以解决不够用的问题
- IPv6:一个128(16字节)的整数,每两个字节为一部分,总共有8部分,每部分用:分隔。例如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b “地球上每一粒沙子都能分配一个”
③特殊的IP地址:
- 局域网IP:192.XXX.XXX.XXX 10.XXX.XXX.XXX
- 广播IP:XXX.XXX.XXX.255 255.255.255.255(全网广播)
- 组播IP:224.XXX.XXX.XXX到239.XXX.XXX.XXX
4. 端口号
①概念:是一个16位的整数,范围:1~65535.
②端口号分类:
- 众所周知的端口:1~1023. (FTP: 21, SSH: 22, HTTP: 80, HTTPS: 469)
- 保留端口:1024~5000 不建议使用
- 可以使用端口:5001~65535
③注意事项:
- TCP端口和UDP端口是相互独立的。(因为它们在内核中的处理路径不同)
- 网络通信由IP地址+端口号来决定。IP地址确定主机位置,端口号确定主机中哪个进程来处理。
5. 字节序
①概念:当涉及内存中存取多字节数据时,就会遇到字节序的问题。
②两种字节序:
- 小端:低字节序内容存储到低地址的内存中。
- 大端:低字节序内容存储到高地址的内存中。
③注意事项:
- 一般本地采用小端模式,网络传输采用大端模式。
- 在发送数据和接收数据前,应该先将本地字节序和网络字节序进行转换。
④主机字节序和网络字节序转换函数:
#include <arpa/inet.h>
// 主要用于网络通信过程中IP和端口的转换
uint16_t htons(uint16_t hostshort); //短整形,主机字节序->网络字节序
uint32_t htonl(uint32_t hostlong); //整形,主机字节序->网络字节序
uint16_t ntohs(uint16_t netshort); //短整形,网络字节序->主机字节序
uint32_t ntohl(uint32_t netlong); //整形,网络字节序->主机字节序
⑤IP地址转换函数:
inet_pton函数:将IP地址从主机字节序->网络字节序。
【1】头文件:#include <arpa/inet.h>
【2】函数原型:int inet_pton(int af, const char *src, void *dst);
【3】参数说明:
- af:地址族,填写AF_INET(IPv4)或者AF_INET6(IPv6)
- src:传入参数,即要转换的IP地址。例如:192.168.5.11
- dst:传出参数,存放转换后的IP地址。
【4】返回值:成功返回1,第一个参数无效返回-1,第二个参数无效返回0.
inet_ntop函数:将IP地址从网络字节序->主机字节序。
【1】头文件:#include <arpa/inet.h>
【2】函数原型:const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
【3】参数说明:
- af:地址族,填写AF_INET(IPv4)或者AF_INET6(IPv6)
- src:传入参数,其中存储了网络字节序的IP地址。
- dst:传出参数,存放转换后的IP地址。
- size:dst指向的内存中最多可以存储多少个字节。
【4】返回值:成功返回指针指向第三个参数对应的地址,失败返回NULL。
二、网络编程常用API
以下函数除read和write外都需包含头文件#include <sys/types.h>、#include <sys/socket.h>
1. socket函数
【1】功能:创建网络套接字。
【2】函数原型:int socket(int domain, int type, int protocol);
【3】参数说明:
domain | AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 | |
AF_LOCAL | 本地通信 | |
type | SOCK_STREAM | 流式套接字,对应TCP |
SOCK_DGRAM | 数据报套接字,对应UDP | |
SOCK_RAW | 原始套接字 | |
protocol | 一般写0即可,使用默认协议。非0一般用于原始套接字。 |
【4】返回值:成功返回套接字fd,失败返回-1。
【5】代码示例:
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket error");
return -1;
}
2. bind函数
【1】功能:将本地的IP、端口与套接字绑定。
【2】函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
【3】参数说明:
sockfd | 套接字,由socket函数生成 | |
addr | struct sockaddr类型的变量的地址 | |
addrlen | addr指向的内存的地址大小 |
struct sockaddr结构体:(写数据时一般不用)
struct sockaddr { sa_family_t sa_family; // 地址族协议, ipv4 char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节) }
struct sockaddr_in结构体:(常用)一般将端口和IP地址保存在该类型的变量中,然后强转为struct sockaddr类型。(它们的大小完全相同)
struct sockaddr_in { sa_family_t sin_family; /* 地址族 */ in_port_t sin_port; /* 端口号, 2字节 -> 网路字节序 */ struct in_addr sin_addr; /* IP地址, 4字节 -> 网络字节序 */ /* 填充8字节,初始化为0 */ unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) - sizeof (in_port_t) - sizeof (struct in_addr)]; }; struct in_addr { in_addr_t s_addr; };
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
#define SERV_PORT 9999
#define SERV_IP "192.168.5.12"
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; //IPv4协议
sin.sin_port = htons(SERV_PROT); //端口号,转化为网络字节序
//IP地址,转化为网络字节序
if (inet_pton(AF_INET, SERV_IP, &sin.sin_addr.s_addr) != 1) {
perror("inet_pton error");
return -1;
}
if (bind(fd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("bind error");
return -1;
}
3. listen函数
【1】功能:设置套接字监听。调用之前需要bind绑定。
【2】函数原型:int listen(int sockfd, int backlog);
【3】参数说明:
sockfd | 监听的文件描述符,由socket函数生成 | |
backlog | 同时处理的最大连接数,一般可取5,最大值为128。表示系统允许2*backlog+1个客户端同时进行三次握手。 |
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
if (listen(fd, 128)) {
perror("listen error");
return -1;
}
4. accept函数
【1】功能:服务器阻塞等待客户端的连接请求,建立新连接,得到通信使用的套接字。
【2】函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
【3】参数说明:
sockfd | 监听的文件描述符,由socket函数生成 | |
addr | 传出参数,保存客户端的地址信息 | |
addrlen | addr指向的内存的大小 |
【4】返回值:成功返回与客户端进行通信的套接字,失败返回-1。
【5】代码示例:
struct sockaddr_in clientAddr;
int clientLen = sizeof(clientAddr);
int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);
if (cfd < 0) {
perror("accept error");
return -1;
}
5. connect函数
【1】功能:客户端发起连接请求。成功连接服务器后,客户端会自动随机绑定一个端口。默认是阻塞的。
【2】函数原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
【3】参数说明:
sockfd | 套接字,由socket函数生成 | |
addr | 存储要连接的服务端的IP和端口信息(需要网络字节序) | |
addrlen | addr指向的内存的大小 |
【4】返回值:成功返回0,失败返回-1。
【5】代码示例:
#define SERV_PORT 9999
#define SERV_IP "192.168.5.12"
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;//IPv4协议
sin.sin_port = htons(SERV_PORT);//目标服务端的端口号,网络字节序
//目标服务端的IP地址,网络字节序
if (inet_pton(AF_INET, SERV_IP, &sin.sin_addr.s_addr) != 1) {
perror("inet_pton error");
return -1;
}
//调用connect函数
if (connect(fd, (struct sockaddr*)&sin, sizeof(sin)) == -1) {
perror("connect error");
return -1;
}
6. read和recv函数
【1】功能:接收数据。
【2】函数原型:
- ssize_t read(int sockfd, void *buf, size_t len);
- ssize_t recv(int sockfd, void *buf, size_t len, int flags);
【3】参数说明:
sockfd | 用于通信的文件描述符 | |
buf | 存储接收的数据 | |
len | buf指向的内存的容量 | |
flags | 特殊属性,一般不使用,指定为0。常用:①MSG_DONTWAIT是非阻塞版本;②MSG_OOB用于发送TCP类型的带外数据;③MSG_PEEK是读取数据并不会从内核中移除数据。 |
【4】返回值:>0表示实际接收的字节数,==0表示对方断开了连接,-1表示失败。
7. write和send函数
【1】功能:接收数据。
【2】函数原型:
- ssize_t write(int sockfd, const void *buf, size_t len);
- ssize_t send(int sockfd, const void *buf, size_t len, int flags);
【3】参数说明:
sockfd | 用于通信的文件描述符 | |
buf | 发送的数据 | |
len | buf的长度 | |
flags | 特殊属性,一般不使用,指定为0。常用:①MSG_DONTWAIT是非阻塞版本;②MSG_OOB用于发送TCP类型的带外数据。 |
【4】返回值:>0表示实际发送的字节数(与len相等),-1表示失败。
8. recvfrom函数
【1】功能:接收数据,一般用于UDP编程。默认是阻塞的。
【2】函数原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
【3】参数说明:
sockfd | 用于通信的文件描述符 | |
buf | 接收的数据 | |
len | buf的长度 | |
flags | 特殊属性,一般不使用,指定为0。常用:①MSG_DONTWAIT是非阻塞版本;②MSG_OOB用于发送TCP类型的带外数据。 | |
src_addr | 保存发送方的ip和端口信息 | |
addrlen | src_addr指向的内存大小 |
【4】返回值:成功返回接收的字节数,失败返回-1。
9. sendto函数
【1】功能:发送数据,一般用于UDP编程。sendto函数默认不阻塞!
【2】函数原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
【3】参数说明:
sockfd | 用于通信的文件描述符 | |
buf | 发送的数据 | |
len | buf的长度 | |
flags | 特殊属性,一般不使用,指定为0。常用:①MSG_DONTWAIT是非阻塞版本;②MSG_OOB用于发送TCP类型的带外数据。 | |
dest_addr | 目标接收方的ip和端口信息 | |
addrlen | dest_addr指向的内存大小 |
【4】返回值:成功返回发送的字节数,失败返回-1。
三、TCP编程
1. TCP介绍
TCP(Transmission Control Protocol)是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:建立连接需要三次握手,断开连接需要四次挥手。
- 可靠安全:在TCP通信过程中,对每个发送的数据包都会进行校验,若数据丢失则重传。
- 流式传输:发送端和接收端的处理速度、数据量都可以不一致。
TCP适用于需要数据可靠传输、实时性不高的场景。
2. TCP通信流程
参考爱编程的大丙:
3. TCP服务端和客户端程序
服务端程序:接收客户端数据并打印,同时将收到的数据重新发回客户端。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define SERV_PORT 9999
int main(int argc, char **argv)
{
/* 1.创建TCP的套接字 */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("server socket error");
return -1;
}
/* 2.绑定IP和端口号 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
addr.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY宏表示本机所有IP
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("server bind error");
return -1;
}
/* 3.设置监听 */
ret = listen(fd, 128);
if (ret == -1) {
perror("server listen error");
return -1;
}
/* 4.等待客户端连接 */
struct sockaddr_in clientAddr;
int clientLen = sizeof(clientAddr);
int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);
if (cfd == -1) {
perror("server accept error");
return -1;
}
//打印客户端信息
char ip[24] = {0};
printf("客户端IP: %s, 端口: %d\n",
inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(clientAddr.sin_port));
/* 5.与客户端通信 */
while (1) {
//接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
ret = read(cfd, buf, sizeof(buf));
if (ret == 0) {
printf("服务端:与客户端断开连接\n");
break;
} else if (ret < 0) {
perror("server read error");
break;
} else {
//打印接收数据,并重新发回接收的数据
printf("服务端接收: %s\n", buf);
write(cfd, buf, sizeof(buf));
}
}
/* 6.关闭文件描述符 */
close(cfd);
close(fd);
return 0;
}
客户端程序:每1秒发送指定数据,同时接收来自服务端的数据。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#define SERV_PORT 9999
#define SERV_IP "192.168.124.6"
int main(int argc, char **argv)
{
/* 1.创建TCP的套接字 */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("client socket error");
return -1;
}
/* 2.与服务端建立连接 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("client connect error");
return -1;
}
//打印连接的服务端信息
printf("连接的服务端IP: %s, 端口: %d\n", SERV_IP, SERV_PORT);
/* 3.与服务端通信 */
int number = 0;
while (1) {
//发送数据
char buf[1024] = {0};
sprintf(buf, "客户端: number = %d\n", number++);
write(fd, buf, sizeof(buf));
//接收数据
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (ret == 0) {
printf("客户端:与服务端断开连接\n");
break;
} else if (ret < 0) {
perror("client read error");
break;
} else {
printf("客户端接收: %s\n", buf);
}
sleep(1);//发送数据慢一些
}
/* 4.关闭文件描述符 */
close(fd);
return 0;
}
4. TCP并发服务器(多进程)
单个服务器需要能够与多个客户端进行通信,因此本节采用多进程方式来实现服务器的并发。
服务器中的父进程:
- 循环accept,每次与客户端建立新连接后,fork子进程。
- sigaction捕捉SIGCHLD信号,回收子进程资源。
服务器中的子进程:负责与新连接的客户端进行通信。
注意事项:accept函数阻塞时,若捕捉到SIGCHLD信号那么会取消阻塞并执行信号处理函数,那么处理完毕后accept函数返回值为-1并且错误号为EINTR。我们需要编写相关代码来处理这种情况,让服务器重新调用accept而不是退出。
如果关闭服务器程序并马上再次运行,会出现server bind error: Address already in use问题。解决策略:允许服务器绑定地址快速重用,在绑定bind前添加下列代码:
int b_reuse = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));
服务器程序:监听所有的客户端连接,每次建立新连接后都创建子进程来与客户端进行通信。子进程读取从客户端接收的数据并显示。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
#define SERV_PORT 6666
/* SIGCHLD处理函数 */
void cycle(int arg)
{
while (1) {
pid_t cPid = waitpid(-1, NULL, WNOHANG);
if (cPid <= 0) {
break;
} else {
printf("子进程%d被回收\n", cPid);
}
}
}
/* 子进程与客户端通信 */
int serverCommu(int fd)
{
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
if (len < 0) {
printf("server read error\n");
} else if (len == 0) {
printf("服务端: 与客户端断开连接\n");
} else {
printf("服务端接收: %s\n", buf);
}
return len;
}
int main(int argc, char **argv)
{
/* 1.创建socket套接字 */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("server socket error");
return -1;
}
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));
/* 2.绑定IP和端口 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0) {
perror("server bind error");
return -1;
}
/* 3.设置监听 */
ret = listen(fd, 5);
if (ret < 0) {
perror("server listen error");
return -1;
}
/* 捕捉信号SIGCHLD, 回收子进程 */
struct sigaction sact;
sact.sa_flags = 0;
sact.sa_handler = cycle;
sigemptyset(&sact.sa_mask);
sigaction(SIGCHLD, &sact, NULL);
/* 4.循环等待客户端连接 */
while (1) {
//accept阻塞等待
struct sockaddr_in clientAddr;
int clientLen = sizeof(clientAddr);
int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);
if (cfd == -1) {
if (errno == EINTR) {//SIGCHLD处理后
continue;
}
perror("server accept error");
break;
}
//打印客户端IP和Port
char IPAddr[24];
printf("连接的客户端IP: %s, 端口: %d\n",
inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, IPAddr, sizeof(IPAddr)),
ntohs(clientAddr.sin_port));
//创建子进程与客户端通信
pid_t pid = fork();
if (pid < 0) {//出错
perror("fork error");
break;
} else if (pid == 0) {//子进程
close(fd);
while (1) {
int len = serverCommu(cfd);
if (len <= 0) {
break;
}
}
close(cfd);
return 0;
} else {//父进程
close(cfd);
}
}
/* 5.父进程关闭套接字 */
close(fd);
return 0;
}
客户端程序:与服务器建立连接,发送用户输入的数据。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#define SERV_PORT 6666
#define SERV_IP "192.168.5.12"
int main(int argc, char **argv)
{
/* 1.创建socket套接字*/
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("client socket error");
return -1;
}
/* 2.建立连接 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0) {
perror("client connect error");
return -1;
}
/* 3.与服务端通信 */
while (1) {
//发送消息
char buf[1024] = {0};
char *tips = "客户端: ";
int tipsLen = strlen(tips);
strcpy(buf, tips);
if (!fgets(buf+tipsLen, sizeof(buf)-tipsLen, stdin)) {
perror("input error");
continue;
}
write(fd, buf, sizeof(buf));
}
/* 4.关闭套接字 */
close(fd);
return 0;
}
测试结果:运行一个服务器程序、两个客户端程序,查看对应的结果。
5. TCP并发服务器(多线程)
本节采用多线程的方式来实现TCP并发服务器。
服务器中的主线程:
- 循环accept,将新建立的连接存储与某个结构体数组元素中。
- 每有一个客户端连接,就创建一个子线程来与其通信。
- 子线程分离,使子线程结束后让系统回收资源。
服务器中的子线程:负责与新连接的客户端进行通信。
注意事项:由于线程只有栈空间是独立的,需要注意数据覆盖的问题。在下面的程序案例中,采用一个全局的结构体数组来保存每一个线程对应操作的通信套接字、线程ID、客户端信息。
服务器程序:监听所有的客户端连接,在建立新连接前找到一块没有被占用的内存,保存建立新连接的通讯套接字、子线程ID和客服端信息。创建的子线程用来和客户端通信。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>
#define SERV_PORT 6666
typedef struct SockInfo {
int fd; //通信的套接字
pthread_t tid; //线程tid
struct sockaddr_in addr;//保存客户端信息
} SockInfo;
SockInfo sInfos[128]; //每个线程对应一个结构体
/* 子线程通信操作 */
void* working(void* arg)
{
SockInfo *sinfo = (SockInfo*)arg;
//打印客户端信息
char IP[24] = {0};
inet_ntop(AF_INET, &sinfo->addr.sin_addr.s_addr, IP, sizeof(IP));
printf("连接的客户端IP: %s, 端口: %d\n", IP, ntohs(sinfo->addr.sin_port));
//接收客户端的信息
while (1) {
char buf[1024] = {0};
int ret = read(sinfo->fd, buf, sizeof(buf));
if (ret < 0) {
perror("server read error");
break;
} else if (ret == 0) {
printf("服务端: 与客户端断开连接\n");
break;
} else {
printf("服务端接收: %s\n", buf);
}
}
//结束前清除数据
sinfo->fd = -1;
sinfo->tid = -1;
memset(&sinfo->addr, 0, sizeof(sinfo->addr));
return NULL;
}
int main(int argc, char **argv)
{
/* 1.创建socket套接字 */
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("server socket error");
return -1;
}
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));
/* 2.绑定IP和端口 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0) {
perror("server bind error");
return -1;
}
/* 3.设置监听 */
ret = listen(fd, 5);
if (ret < 0) {
perror("server listen error");
return -1;
}
/* 4.主线程循环监听,子线程通信 */
int sInfosLen = sizeof(sInfos) / sizeof(sInfos[0]);
//初始化结构体数组
for (int i = 0; i < sInfosLen; i++)
{
sInfos[i].fd = -1;
sInfos[i].tid = -1;
memset(&sInfos[i].addr, 0, sizeof(sInfos[i].addr));
}
//循环监听
while (1) {
//遍历sInfos,找到未占用的元素
SockInfo *pinfo = NULL;
for (int i = 0; i < sInfosLen; i++)
{
if (sInfos[i].fd == -1) {
pinfo = &sInfos[i];
break;
}
if (i == sInfosLen - 1) {//若全部被占用,停留在最后
sleep(1);
--i;
}
}
//存在新连接,创建子线程,保存于pinfo指向的结构体
int sockaddrLen = sizeof(pinfo->addr);
int connfd = accept(fd, (struct sockaddr*)&pinfo->addr, &sockaddrLen);
if (connfd < 0) {
perror("server accept error");
break;
}
pinfo->fd = connfd;
pthread_create(&pinfo->tid, NULL, working, pinfo);
pthread_detach(pinfo->tid);
}
/* 5.主线程关闭套接字 */
close(fd);
return 0;
}
客户端程序:与服务器建立连接,发送用户输入的数据。(与多进程的版本相同)
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#define SERV_PORT 6666
#define SERV_IP "192.168.5.12"
int main(int argc, char **argv)
{
/* 1.创建socket套接字*/
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("client socket error");
return -1;
}
/* 2.建立连接 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0) {
perror("client connect error");
return -1;
}
/* 3.与服务端通信 */
while (1) {
//发送消息
char buf[1024] = {0};
char *tips = "客户端: ";
int tipsLen = strlen(tips);
strcpy(buf, tips);
if (!fgets(buf+tipsLen, sizeof(buf)-tipsLen, stdin)) {
perror("input error");
continue;
}
write(fd, buf, sizeof(buf));
}
/* 4.关闭套接字 */
close(fd);
return 0;
}
测试结果:运行一个服务器程序、两个客户端程序,查看对应的结果。
6. TCP粘包问题
【1】问题描述:服务器在接收客户端发送的数据包时,可能会出现多个数据包无法拆分的情况,这种情况就是粘包问题。
【2】解决措施:在发送数据块前添加一个固定大小的数据头部,其中包含了数据块的总字节数。接收端先接收固定大小的头部信息,然后根据头部内容再读取指定字节数的内容。
【3】注意事项:在大型工程里,通过read和write来发送和接收数据,会存在着一定的大小限制。因此,有可能无法一次性全部读取、发送完毕。针对此现象,可以用循环read和循环write的方式进行数据的接收和发送。
【4】发送端代码:
/**********************************************
* 函 数: 发送指定字节数的数据
* 参 数:
* -sockfd: 通信套接字
* -data: 发送的数据
* -len: 发送的数据长度
* 返回值: 成功返回发送的字节数,失败返回-1
*********************************************/
int writen(int sockfd, const char* data, int len)
{
const char* buf = data;
int count = len;
while (count > 0) {
int ret = write(sockfd, buf, count);
if (ret == -1) {
return -1;
}
else if (ret == 0) {
continue;
}
count -= ret;
buf += ret;
}
return len;
}
/**********************************************
* 函 数: 发送带有头部的数据包
* 参 数:
* -sockfd: 通信套接字
* -msg: 发送的数据
* -len: 发送的数据长度
* 返回值: 成功返回发送的字节数,失败返回-1
*********************************************/
int sendMsg(int sockfd, const char* msg, int len)
{
if (sockfd <= 0 || msg == NULL || len <= 0) {
return -1;
}
//动态申请len+4空间(头部规定为4字节)
char *buf = (char*)malloc(len + 4);
if (buf == NULL) {
printf("sendMsg: malloc error\n");
return -1;
}
//填充数据头部和数据块
int bigLen = htonl(len);//头部数据转换为网络字节序
memcpy(buf, &bigLen, 4);
memcpy(buf+4, msg, len);
//发送buf缓冲区的数据
int ret = writen(sockfd, buf, len+4);
//清空申请的缓冲区
free(buf);
return ret;
}
【5】接收端代码:
/**********************************************
* 函 数: 接收指定字节数的数据
* 参 数:
* -sockfd: 通信套接字
* -data: 存储接收数据的地址
* -len: 接收的数据长度
* 返回值: 成功返回接收的字节数,失败返回-1
*********************************************/
int readn(int sockfd, char* data, int len)
{
char* buf = data;
int count = len;
while (count > 0) {
int ret = read(sockfd, buf, count);
if (ret == -1) {
return -1;
}
else if (ret == 0) {
return len - count;
}
count -= ret;
buf += ret;
}
return len;
}
/************************************************
* 函 数: 接收带有头部的数据包
* 参 数:
* -sockfd: 通信套接字
* -data: 一级指针的地址,存放接收的真实数据
* 返回值: 成功返回接收的字节数,失败返回-1
***********************************************/
int recvMsg(int sockfd, char** data)
{
if (sockfd <= 0 || data == NULL) {
return -1;
}
//接收4字节的头部
int len = 0;
readn(sockfd, (char*)&len, 4);
len = ntohl(len);//转换为本地字节序
//开辟缓冲区,接收真实数据内容
char *buf = (char*)malloc(len+1);
int ret = readn(sockfd, buf, len);
if (ret != len) {
free(buf);
return -1;
}
buf[len] = '\0';
*data = buf;
return ret;
}
四、UDP编程
1. UDP介绍
UDP(User Datagram Protocol)是面向无连接的、不安全的报式传输层通信协议。
- UDP没有信息确认机制,发送端不知道发送的消息是否丢失。
- 消息丢失了也不会重发。
- UDP发送数据采用sendto函数,默认不阻塞,并且不会将数据写入内核的缓冲区中。
UDP适用于实时性较高、安全性较低的场景,例如:视频聊天等。
2. UDP通信流程
参考爱编程的大丙:
3. UDP服务端和客户端程序
服务端:接收来自客户端发送的数据,并打印客户端信息。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define SERV_PORT 9999
int main(int argc, char **argv)
{
/* 1.创建socket套接字 */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
perror("server socket error");
return -1;
}
/* 2.绑定IP和端口 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(SERV_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("server bind error");
close(fd);
return -1;
}
/* 3.接收客户端信息 */
char buf[1024];
char ip[16];
struct sockaddr_in clientAddr;
int clientLen = sizeof(clientAddr);
while (1) {
memset(buf, 0, sizeof(buf));//清空缓冲区
int recvLen = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&clientAddr, &clientLen);//接收数据
if (recvLen == 0) {
continue;
}
inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, ip, sizeof(ip));//ip地址抓换为本地字节序
printf("来自客户端(%s:%d): %s\n", ip, ntohs(clientAddr.sin_port), buf);//打印接收的信息
}
/* 4.关闭套接字 */
close(fd);
return 0;
}
客户端:由用户输入内容,将内容发送至服务端。
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define SERV_PORT 9999
#define SERV_IP "192.168.5.12"
int main(int argc, char **argv)
{
/* 1.创建socket套接字 */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
perror("server socket error");
return -1;
}
/* 初始化目标服务端地址 */
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, SERV_IP, &serverAddr.sin_addr.s_addr);
/* 2.客户端发送信息 */
char buf[1024];
while (1) {
//用户输入内容
memset(buf, 0, sizeof(buf));
if (!fgets(buf, sizeof(buf), stdin)) {
break;
}
//发送给指定服务端
sendto(fd, buf, sizeof(buf), 0, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
}
/* 3.关闭套接字 */
close(fd);
return 0;
}
测试结果:运行两个客户端程序、一个服务端程序。两个客户端程序向服务端程序发送信息,服务端打印接收到的信息。
4. UDP广播
UDP广播是一种批量发送数据的机制,一般由一个广播端发送给多个接收端。
- 每个网段都有特殊的广播地址,例如192.168.5.255(最后是255)。
- 所有绑定相同网段和指定端口的进程都会接收到广播的数据,例如192.168.5.11和192.168.5.16等。
- 广播只能在局域网内使用,无法应用于广域网。
- 接收端无法拒绝广播的消息,除非关闭接收端进程。
广播端需要开启广播属性:
int opt = 1; setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
广播端程序:广播发送用户输入的数据。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define RECV_PORT 9999
#define RECV_BROADCAST "192.168.5.255"
int main(int argc, char **argv)
{
/* 1.创建socket套接字 */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
perror("broadcast socket error");
return -1;
}
/* 2.开启广播属性 */
int b_broadcast = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &b_broadcast, sizeof(b_broadcast));
/* 3.广播数据 */
char buf[1024];
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(RECV_PORT);
inet_pton(AF_INET, RECV_BROADCAST, &addr.sin_addr.s_addr);
while (1) {
//用户输入数据
memset(buf, 0, sizeof(buf));
if (!fgets(buf, sizeof(buf), stdin)){
perror("input error");
break;
}
//广播数据
sendto(fd, buf, sizeof(buf), 0, (struct sockaddr*)&addr, sizeof(addr));
}
/* 4.关闭套接字 */
close(fd);
return 0;
}
接收端程序:接收广播端程序的数据并打印。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define RECV_PORT 9999
int main(int argc, char **argv)
{
/* 1.创建socket套接字 */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
perror("broadcast socket error");
return -1;
}
/* 允许地址可重用 */
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(b_reuse));
/* 2.接收端绑定端口 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(RECV_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("recv bind error");
close(fd);
return -1;
}
/* 3.接收数据 */
char buf[1024] = {0};
char IP[16] = {0};
struct sockaddr_in broadAddr;
int broadAddrLen = sizeof(addr);
while (1) {
memset(buf, 0, sizeof(buf));//清空缓冲区
recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&broadAddr, &broadAddrLen);//接收
inet_ntop(AF_INET, &broadAddr.sin_addr.s_addr, IP, sizeof(IP));
printf("接收数据(%s:%d): %s\n", IP, ntohs(broadAddr.sin_port), buf);
}
/* 4.关闭套接字 */
close(fd);
return 0;
}
测试结果:运行两个接收端程序和一个广播端程序,结果如下:
5. UDP组播(多播)
UDP组播(多播)是一种允许一个或多个组播源发送同一报文到组内多个接收者的技术。
- 发送端发送同一报文给一个“组”,组内的成员都能收到该报文。
- 组播的IP地址范围为:224.0.0.0~239.255.255.255,但是内部仍有规定,如下所示:(来源于爱编程的大丙)
- 广播只能用于局域网,而组播能用于广域网。
- 发送端和接收端均需要设置组播属性。
发送端设置组播属性:
struct in_addr opt; inet_pton(AF_INET, "239.0.0.1", &opt.s_addr);//组播地址转换 setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &opt, sizeof(opt));
接收端设置组播属性:
struct ip_mreqn opt; inet_pton(AF_INET, "239.0.0.1", &opt.imr_multiaddr.s_addr);//组播地址 opt.imr_address.s_addr = INADDR_ANY;//本地地址 opt.imr_ifindex = if_nametoindex("ens33");//将网卡名转换为网卡的编号 setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &opt, sizeof(opt));
上述涉及的结构体:
struct in_addr { in_addr_t s_addr; // unsigned int }; struct ip_mreqn { struct in_addr imr_multiaddr; //组播地址 struct in_addr imr_address; //本地地址 int imr_ifindex; //网卡编号, 每个网卡都有一个编号 };
发送端程序:组播用户输入的数据。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define GROUP_PORT 9999
#define GROUP_IP "239.0.0.1"
int main(int argc, char **argv)
{
/* 1.创建socket套接字 */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
perror("broadcast socket error");
return -1;
}
/* 2.设置组播属性 */
struct in_addr opt;
inet_pton(AF_INET, GROUP_IP, &opt.s_addr);
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &opt, sizeof(opt));
/* 3.组播数据 */
char buf[1024];
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(GROUP_PORT);
inet_pton(AF_INET, GROUP_IP, &addr.sin_addr.s_addr);
while (1) {
//用户输入数据
memset(buf, 0, sizeof(buf));
if (!fgets(buf, sizeof(buf), stdin)){
perror("input error");
break;
}
//组播数据
sendto(fd, buf, sizeof(buf), 0, (struct sockaddr*)&addr, sizeof(addr));
}
/* 4.关闭套接字 */
close(fd);
return 0;
}
接收端程序:接收组播的数据并打印。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <net/if.h>
#define GROUP_PORT 9999
#define GROUP_IP "239.0.0.1"
int main(int argc, char **argv)
{
/* 1.创建socket套接字 */
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd == -1) {
perror("broadcast socket error");
return -1;
}
/* 允许地址可重用 */
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(b_reuse));
/* 2.接收端绑定端口 */
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(GROUP_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("recv bind error");
close(fd);
return -1;
}
/* 3.设置组播属性 */
struct ip_mreqn opt;
inet_pton(AF_INET, GROUP_IP, &opt.imr_multiaddr.s_addr);//组播地址
opt.imr_address.s_addr = INADDR_ANY;//本地地址
opt.imr_ifindex = if_nametoindex("ens33");//网卡名转换为编号
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &opt, sizeof(opt));
/* 4.接收数据 */
char buf[1024] = {0};
char IP[16] = {0};
struct sockaddr_in groupAddr;
int groupAddrLen = sizeof(addr);
while (1) {
memset(buf, 0, sizeof(buf));//清空缓冲区
recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr*)&groupAddr, &groupAddrLen);//接收
inet_ntop(AF_INET, &groupAddr.sin_addr.s_addr, IP, sizeof(IP));
printf("接收数据(%s:%d): %s\n", IP, ntohs(groupAddr.sin_port), buf);
}
/* 5.关闭套接字 */
close(fd);
return 0;
}
测试结果:运行一个发送端程序、两个接收端程序。
五、IO多路复用
背景动机:在第四章中,我们采用了多进程/多线程的方式实现了TCP服务器的并发,但是这种方式有时需要进程/线程的同步和通信机制,使程序变得更为复杂。因此,可以使用IO多路复用(转接)。
IO多路复用的优势:单进程/单线程中也能实现TCP服务器的并发,系统开销小。
IO多路复用基本原理:
①把关心的文件描述符fd加入到集合中。
②调用select、poll等函数委托内核监控集合中的文件描述符,阻塞等待一个或多个文件描述符有数据。(其实就是检测文件描述符对应的读写缓冲区状态)
③当有数据时,退出select、poll等函数。
④依次判断哪个文件描述符有数据,并依次处理对应的数据。
注意事项:
- 一个进程默认最多有1024个文件描述符。因此,集合fd_set的大小为128字节,每一位对应一个文件描述符,共有128*8=1024位,即1024个文件描述符。
- IO多路复用不仅适用于TCP服务端的并发,还适用于普通的文件描述符。
1. select方式
【1】功能:select支持Linux、Mac、Windows平台。该函数默认是阻塞的,委托内核检测文件描述符的状态,实则检测对应缓冲区的状态。底层实现是数组。
【2】头文件:#include <sys/select.h>
【3】函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
【4】参数说明:
- nfds:委托内核检测的三个集合中最大的文件描述符+1。
- readfds:传入传出参数,读集合。
- writefds:传入传出参数,写集合。
- exceptfds:传入传出参数,异常集合。
- timeout:超时时长,用于解除select函数的阻塞。
【5】返回值:①成功返回集合中已就绪的文件描述符的总个数;②失败返回-1;③超时返回0.
集合fd_set操作函数:在集合中,标志位为1表示检测,0表示不检测。
void FD_CLR(int fd, fd_set *set);//将fd对应的标志位设置为0 void FD_SET(int fd, fd_set *set);//将fd对应的标志位设置为1 void FD_ZERO(fd_set *set);//将set中所有标志位设置为0 int FD_ISSET(int fd, fd_set *set);//判断fd的标志位是0还是1
【6】编写流程:来源于爱编程的大丙。
程序实例:通过select方式实现服务器并发。
服务端程序:委托内核来检测:①监听的socket套接字(是否有新连接);②通信fd(读缓冲区是否有数据,与客户端通信)。
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/select.h> #define SERV_PORT 9999 #define SERV_IP "192.168.5.12" int main(int argc, char **argv) { /* 1.创建socket套接字 */ int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd == -1) { perror("server socket error"); return -1; } /* 允许地址快速重用*/ int b_reuse = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(b_reuse)); /* 2.绑定ip和端口 */ struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(SERV_PORT); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if (ret == -1) { perror("server bind error"); close(fd); return -1; } /* 3.设置监听 */ ret = listen(fd, 5); if (ret == -1) { perror("server listen error"); close(fd); return -1; } /* 4.初始化fd集合 */ int maxfd = fd; fd_set rdset; //保存原始的fd fd_set rdtemp; //保存内核修改过的fd //初始化rdset FD_ZERO(&rdset); FD_SET(fd, &rdset);//检测监听的fd /* 5.与多个客户端通信 */ while (1) { //委托内核检测 rdtemp = rdset; ret = select(maxfd+1, &rdtemp, NULL, NULL, NULL); if (ret == -1) { perror("server select error"); break; } //针对新连接 if (FD_ISSET(fd, &rdtemp)) { struct sockaddr_in clientAddr; int clientAddrLen = sizeof(clientAddr); int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientAddrLen); if (cfd < 0) { perror("server accept error"); continue; } FD_SET(cfd, &rdset); maxfd = cfd > maxfd ? cfd : maxfd; //打印新连接的客户端信息 char IP[16]; printf("与客户端%s:%d建立连接!\n", inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, IP, sizeof(IP)), ntohs(clientAddr.sin_port)); } //与多个客户端通信 for (int i = 0; i < maxfd + 1; ++i) { //跳过监听的fd和没有就绪的fd if (i == fd || !FD_ISSET(i, &rdtemp)) { continue; } //接收客户端发送的数据 char buf[1024] = {0}; int len = read(i, buf, sizeof(buf)); if (len == 0) { printf("断开连接: 客户端退出\n"); FD_CLR(i, &rdset);//从rdset中清除 close(i); } else if (len > 0) { printf("服务端接收: %s\n", buf);//打印接收的数据 write(i, buf, strlen(buf)+1);//发送接收到的数据 } else { perror("server read error"); } } } /* 6.释放资源 */ close(fd);//关闭监听的fd return 0; }
客户端程序:①由用户输入数据,并发送给服务端;②读取服务端发送的数据。由于fgets函数和read函数均会阻塞,所以此处也采用了select方式实现IO多路复用。
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/select.h> #define SERV_PORT 9999 #define SERV_IP "192.168.5.12" int main(int argc, char **argv) { /* 1.创建socket套接字 */ int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd == -1) { perror("client socket error"); return -1; } /* 2.建立连接 */ struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr); int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); if (ret == -1) { perror("client connect error"); close(fd); return -1; } /* 3.与服务端通信 */ char buf[1024]; int maxfd = fd; while (1) { //委托内核检测0和fd fd_set rdset; FD_ZERO(&rdset); FD_SET(0, &rdset); FD_SET(fd, &rdset); ret = select(maxfd+1, &rdset, NULL, NULL, NULL); if (ret == -1) { perror("client select error"); break; } //发送数据,由用户输入 if (FD_ISSET(0, &rdset)) { memset(buf, 0, sizeof(buf)); if (!fgets(buf, sizeof(buf)-1, stdin)) { perror("client input error"); break; } write(fd, buf, strlen(buf)+1); } //读取数据,来自服务端 if (FD_ISSET(fd, &rdset)) { memset(buf, 0, sizeof(buf)); int len = read(fd, buf, sizeof(buf)); if (len == 0) { printf("断开连接: 服务端退出!\n"); break; } else if (len > 0) { printf("客户端接收: %s\n", buf); } else { perror("client read error"); } } } /* 4.释放资源 */ close(fd); return 0; }
测试结果:运行两个客户端程序、一个服务端程序。
2. poll方式
【1】说明:poll方式与select方式类似,委托内核线性检测文件描述符集合。底层实现是链表。
【2】函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
【3】参数说明:
- fds:是struct pollfd类型的文件描述符集合。
- nfds:是fds最后一个有效元素的下标+1.
- timeout:指定poll函数的阻塞时长。
- -1:一直阻塞,直到有文件描述符就绪。
- 0:不阻塞,函数马上返回。
- >0:指定阻塞的毫秒数。
【4】返回值:成功返回>0表示已就绪的文件描述符个数,失败返回-1.
struct pollfd结构体如下:参考爱编程的大丙。其中revents成员不需要手动赋值,poll函数内部会清除该成员。
struct pollfd { int fd; /* 委托内核检测的文件描述符 */ short events; /* 委托内核检测文件描述符的什么事件 */ short revents; /* 文件描述符实际发生的事件 -> 传出 */ };
程序实例:通过poll方式实现服务器并发。
服务端程序:委托内核来检测:①监听的socket套接字(是否有新连接);②通信fd(读缓冲区是否有数据,与客户端通信)。
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <poll.h> #define SERV_PORT 9999 #define SERV_IP "192.168.5.12" int main(int argc, char **argv) { /* 1.创建socket套接字 */ int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd == -1) { perror("server socket error"); return -1; } /* 允许地址快速重用*/ int b_reuse = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(b_reuse)); /* 2.绑定ip和端口 */ struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(SERV_PORT); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if (ret == -1) { perror("server bind error"); close(fd); return -1; } /* 3.设置监听 */ ret = listen(fd, 5); if (ret == -1) { perror("server listen error"); close(fd); return -1; } /* 4.初始化fd集合 */ struct pollfd fds[1024]; for (int i = 0; i < 1024; ++i) { fds[i].fd = -1; fds[i].events = POLLIN; } fds[0].fd = fd; int maxIndex = 0;//fds数组中最后一个有效元素下标 /* 5.与客户端通信 */ while (1) { //委托内核检测 ret = poll(fds, maxIndex+1, -1); if (ret == -1) { perror("server poll error"); break; } //针对新连接 if (fds[0].revents & POLLIN) {//换成==也可以 struct sockaddr_in clientAddr; int clientAddrLen = sizeof(clientAddr); int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientAddrLen); if (cfd < 0) { perror("server accept error"); continue; } //寻找fds中空闲的位置填入cfd int i; for (i = 0; i < 1024; ++i) { if (fds[i].fd == -1) { fds[i].fd = cfd; break; } } maxIndex = i > maxIndex ? i : maxIndex; //打印新连接的客户端信息 char IP[16]; printf("与客户端%s:%d建立连接!\n", inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, IP, sizeof(IP)), ntohs(clientAddr.sin_port)); } //与客户端通信 for (int i = 0; i < maxIndex + 1; ++i) { //跳过监听的fd和没有就绪的fd if (fds[i].fd == fd || !(fds[i].revents & POLLIN)) { continue; } //接收客户端发送的数据 char buf[1024] = {0}; int len = read(fds[i].fd, buf, sizeof(buf)); if (len == 0) { printf("断开连接: 客户端退出\n"); close(fds[i].fd); fds[i].fd = -1;//从集合fds中清除 } else if (len > 0) { printf("服务端接收: %s\n", buf);//打印接收的数据 write(fds[i].fd, buf, strlen(buf)+1);//发送接收到的数据 } else { perror("server read error"); } } } /* 6.释放资源 */ close(fd);//关闭监听的fd return 0; }
客户端程序:①由用户输入数据,并发送给服务端;②读取服务端发送的数据。由于fgets函数和read函数均会阻塞,所以此处采用了poll方式实现IO多路复用。
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <poll.h> #define SERV_PORT 9999 #define SERV_IP "192.168.5.12" int main(int argc, char **argv) { /* 1.创建socket套接字 */ int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd == -1) { perror("client socket error"); return -1; } /* 2.建立连接 */ struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr); int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); if (ret == -1) { perror("client connect error"); close(fd); return -1; } /* 初始化文件描述符集合 */ struct pollfd fds[2]; fds[0].fd = 0; fds[0].events = POLLIN; fds[1].fd = fd; fds[1].events = POLLIN; /* 3.与服务端通信 */ char buf[1024]; while (1) { //委托内核检测0和fd ret = poll(fds, 2, -1); if (ret == -1) { perror("client poll error"); break; } //发送数据,由用户输入 if (fds[0].revents & POLLIN) { memset(buf, 0, sizeof(buf)); if (!fgets(buf, sizeof(buf)-1, stdin)) { perror("client input error"); break; } write(fd, buf, strlen(buf)+1); } //读取数据,来自服务端 if (fds[1].revents & POLLIN) { memset(buf, 0, sizeof(buf)); int len = read(fd, buf, sizeof(buf)); if (len == 0) { printf("断开连接: 服务端退出!\n"); break; } else if (len > 0) { printf("客户端接收: %s\n", buf); } else { perror("client read error"); } } } /* 4.释放资源 */ close(fd); return 0; }
测试结果:运行两个客户端程序、一个服务端程序。
3. epoll方式
【1】说明:是IO多路复用的另一种方式,比select和poll效率更高。底层是红黑树。
【2】epoll_create函数:创建epoll实例,通过红黑树管理待检测集合。
int epoll_create(int size);
//size: Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值
//返回值:成功返回epoll实例相关的文件描述符,失败返回-1
【3】epoll_ctl函数:管理红黑树实例上的节点,可以进行添加、删除、修改操作。注意:添加的节点信息是event,若该节点的fd就绪了,那么该信息就会被作为数组的元素传出,通过访问数组元素的event.data.fd就能获取就绪的文件描述符。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd: epoll_create函数的返回值,通过该参数访问epoll实例
//op:
//EPOLL_CTL_ADD:往红黑树中添加新的节点
//EPOLL_CTL_MOD:修改红黑树中已经存在的节点
//EPOLL_CTL_DEL:删除红黑树中的指定的节点
//fd: 文件描述符,即要添加、修改、删除的文件描述符
//event: 指定检测文件描述符fd的事件
//返回值:成功返回0,失败返回-1
struct epoll_event {
//events包括EPOLLIN(读事件)、EPOLLOUT(写事件)、EPOLLERR(异常事件)
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd; //一般使用该成员, 和epoll_ctl的第三个参数相同
uint32_t u32;
uint64_t u64;
} epoll_data_t;
【4】epoll_wait函数:检测创建的epoll实例中是否有就绪的文件描述符。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//epfd: epoll_create函数的返回值
//events: 传出参数,是结构体数组地址,保存已就绪的文件描述符信息
//maxevents: 结构体数组的容量
//timeout: 阻塞时长,以毫秒为单位
//-1: 一直阻塞
//0: 不阻塞
//>0: 函数阻塞对应的毫秒数再返回
//返回值:超时退出则返回0,检测到就绪返回就绪的文件描述符数量,失败返回-1
【5】epoll工作模式:详见 IO多路转接(复用)之epoll | 爱编程的大丙 (subingwen.cn)
程序实例:通过epoll方式实现服务器并发。
服务端程序:委托内核来检测:①监听的socket套接字(是否有新连接);②通信fd(读缓冲区是否有数据,与客户端通信)。
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/epoll.h> #define SERV_PORT 9999 #define SERV_IP "192.168.5.12" int main(int argc, char **argv) { /* 1.创建socket套接字 */ int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd == -1) { perror("server socket error"); return -1; } /* 允许地址快速重用*/ int b_reuse = 1; setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(b_reuse)); /* 2.绑定ip和端口 */ struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(SERV_PORT); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if (ret == -1) { perror("server bind error"); close(fd); return -1; } /* 3.设置监听 */ ret = listen(fd, 5); if (ret == -1) { perror("server listen error"); close(fd); return -1; } /* 4.创建epoll实例并添加fd */ int epfd = epoll_create(100);//创建epoll实例 if (epfd == -1) { perror("server epoll error"); close(fd); return -1; } struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = fd; ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);//添加监听的fd if (ret == -1) { perror("server epoll_ctl error"); close(fd); return -1; } /* 5.循环检测 */ struct epoll_event evs[1024]; int evsLen = sizeof(evs) / sizeof(evs[0]); while (1) { //委托内核检测 int num = epoll_wait(epfd, evs, evsLen, -1); if (num == -1) { perror("server epoll error"); break; } //遍历evs从0到num for (int i = 0; i < num; ++i) { if (evs[i].data.fd == fd) {//监听 struct sockaddr_in clientAddr; int clientAddrLen = sizeof(clientAddr); int cfd = accept(fd, (struct sockaddr*)&clientAddr, &clientAddrLen); if (cfd < 0) { perror("server accept error"); continue; } //添加cfd到红黑树中 ev.events = EPOLLIN; ev.data.fd = cfd; ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);//添加监听的fd if (ret == -1) { perror("server epoll_ctl error"); close(fd); return -1; } //打印新连接的客户端信息 char IP[16]; printf("与客户端%s:%d建立连接!\n", inet_ntop(AF_INET, &clientAddr.sin_addr.s_addr, IP, sizeof(IP)), ntohs(clientAddr.sin_port)); } else {//与客户端通信 char buf[1024] = {0}; int len = read(evs[i].data.fd, buf, sizeof(buf)); if (len < 0) { perror("server read error"); } else if (len == 0) { printf("断开连接: 客户端退出!\n"); epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, NULL);//从红黑树中删除 close(evs[i].data.fd);//这里必须先删除再close,否则epoll_ctl失败 } else { printf("服务端接收: %s\n", buf); write(evs[i].data.fd, buf, strlen(buf)+1); } } } } /* 6.释放资源 */ close(fd);//关闭监听的fd return 0; }
客户端程序:①由用户输入数据,并发送给服务端;②读取服务端发送的数据。由于fgets函数和read函数均会阻塞,所以此处采用了poll方式实现IO多路复用。
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <poll.h> #define SERV_PORT 9999 #define SERV_IP "192.168.5.12" int main(int argc, char **argv) { /* 1.创建socket套接字 */ int fd = socket(AF_INET, SOCK_STREAM, 0); if (fd == -1) { perror("client socket error"); return -1; } /* 2.建立连接 */ struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &addr.sin_addr.s_addr); int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); if (ret == -1) { perror("client connect error"); close(fd); return -1; } /* 初始化文件描述符集合 */ struct pollfd fds[2]; fds[0].fd = 0; fds[0].events = POLLIN; fds[1].fd = fd; fds[1].events = POLLIN; /* 3.与服务端通信 */ char buf[1024]; while (1) { //委托内核检测0和fd ret = poll(fds, 2, -1); if (ret == -1) { perror("client poll error"); break; } //发送数据,由用户输入 if (fds[0].revents & POLLIN) { memset(buf, 0, sizeof(buf)); if (!fgets(buf, sizeof(buf)-1, stdin)) { perror("client input error"); break; } write(fd, buf, strlen(buf)+1); } //读取数据,来自服务端 if (fds[1].revents & POLLIN) { memset(buf, 0, sizeof(buf)); int len = read(fd, buf, sizeof(buf)); if (len == 0) { printf("断开连接: 服务端退出!\n"); break; } else if (len > 0) { printf("客户端接收: %s\n", buf); } else { perror("client read error"); } } } /* 4.释放资源 */ close(fd); return 0; }
测试结果:运行两个客户端程序、一个服务端程序。
4. 优缺点
【1】select方式:(跨平台)
优点:支持多种平台,适合跨平台使用。
缺点:①文件描述符最多为1024。②在用户态和内核态之间会频繁切换,且会频繁拷贝文件描述集合,效率低。③内核检测文件描述符集合是线性的,对于文件描述符数量多的场景检测效率低。
【2】poll方式:(不常用)
优点:由于底层是链表,没有文件描述符数量的限制。
缺点:①无法跨平台。②在用户态和内核态之间会频繁切换,且会频繁拷贝文件描述集合,效率低。③内核检测文件描述符集合是线性的,对于文件描述符数量多的场景检测效率低。
【3】epoll方式:(高效)
优点:①采用红黑树和回调机制,内核检测的效率更高。②采用共享内存(mmap内存映射区实现),无需在用户区和内核区之间进行集合的拷贝。③事件响应,能够直接得到已就绪的文件描述符,无需手动遍历文件描述符集合。④没有文件描述符的数量限制。
缺点:无法跨平台,只能在Linux中。