1.select模型
1.1什么是select模型
select模是一种用于管理和处理多个文件描述符的I/O多路复用技术,尤其在网络编程中非常常见。他的主要作用是允许程序同时监控多个文件描述符,判断它们是否可以进行读写操作或有异常发送。
1.2为什么要用select模型
1)处理多个连接,使用select,服务器可以在单个线程或进程中同时处理多个客户端连接,而无需为每个客户端创建一个线程或进程。极大的简化了编程模型并节省了系统资源
2)避免阻塞,直接在套接字上调用read()或write(),这些操作可能会阻塞,等待数据的到来或发送完成、使用select可以先检测哪些套接字准备好进行I/O操作。这样就可以避免在为准备好的套接字上阻塞
3)可以高效的利用资源,在高并发的情况下,如果为每一个连接都创建一个线程或进程,将要消耗大量的系统资源。select模型通过在单个线程或进程中处理多个I/O操作,能更高效地利用系统资源,特别是在连接数多单每个连接到I/O频率较低的场景。
4)兼容性好,大多数Unix系统和window系统的支持。因此可以跨平台使用。
5)相对其他的I/O多路复用技术,select的API相对简单,易于理解和使用
1.3使用场景
1)小型服务器
2)网络代理
3)非阻塞I/O
1.4函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
参数解释
1.int nfds :文件描述符数量,通常是文件描述符集合中最大的文件描述符加1(max_fd+1)
2.fd_set *readfds:指向一个fd_set类型的指针,代表你感兴趣的可写事件的文件描述符集合,如果某个文件描述符在readfds集合中且有数据可读,select返回时将保留该文件描述符。如果你不关心任何可写事件,可以传递 NULL
。
3.fd_set *writefds指向一个fd_set类型的指针,代表你感兴趣的异常事件(如带外数据)的文件描述符集合。与前面类似
4.fd_set *exceptfds与前面类似,代表异常事件。
5.struct timeval *timeout
:指向一个’timeval‘结构的指针,用于设置select函数的超时时间,0是
timeval
结构体的定义如下
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
- 如果
timeout
为NULL
,select
会一直阻塞,直到某个文件描述符状态发生变化。 - 如果
timeout
的tv_sec
和tv_usec
都为 0,select
将立即返回,不阻塞。
返回值:
成功:返回准备好的文件描述符数量中的总数。
失败返回-1并设置errno,指出错误原因
超时:如果在指定的时间内没有任何文件描述符准备好,则返回0
其他定义与宏解释
fd_set:是一个结构体,用于表示文件描述符的集合。
FD_ZERO(fd_set *set):初始化一个 fd_set
变量,将所有位清零。
FD_SET(int fd, fd_set *set):将指定的文件描述符 fd
添加到文件描述符集合 set
中。也就是将 fd
对应的位设置为 1
FD_CLR(int fd, fd_set *set):将指定的文件描述符 fd
从集合 set
中移除。
FD_ISSET(int fd, fd_set *set):检查 fd
是否在集合 set
中。也就是检查 fd
对应的位是否为 1。如果文件描述符 fd
在集合 set
中,返回非零值;否则返回 0。
1.5工作流程
1)初始化文件描述符集合:用“FD_ZERO”函数初始化一个‘fd_set’结构体,用于存储需要监视的文件描述符。
2)将文件描述符添加到集合中,使用‘FD_SET’函数将感兴趣的文件描述符添加到‘fd_set'中。
3)调用select函数:select返回时会阻塞当前进程,直到监视的文件描述符中至少有一个文件描述符的状态发生变化,或者指定的超时时间到达
4)处理就绪的文件描述符:当select返回时,程序检查哪些文件描述符已经准备好进行I/O操作,并进行相应处理
5)重复上述过程
1.6样例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
// 宏定义端口号
#define PROT 8083
int server;
void handle_signal(int sig)
{
if(sig==SIGINT)
{
printf("\nServer shutting down...\n");
close(server); // 关闭服务器套接字
exit(0);
}
}
int main()
{
char buf[1024];
struct sockaddr_in sock_addr;
fd_set readfds,fds;
int max_fd =0;
//创建套接字
server=socket(AF_INET,SOCK_STREAM,0);
if(server<0)
{
perror("socket");
_exit(EXIT_FAILURE);
}
//接收CTRL+C
signal(SIGINT,handle_signal);
//配置并绑定ip与端口号
sock_addr.sin_family=AF_INET;
sock_addr.sin_port=htons(PROT);
sock_addr.sin_addr.s_addr=INADDR_ANY;
if(bind(server,(struct sockaddr *)&sock_addr,sizeof(sock_addr))<0)
{
perror("bind");
_exit(EXIT_FAILURE);
}
//监听套接字
if(listen(server,100)<0)
{
perror("listen");
_exit(EXIT_FAILURE);
}
//初始化fds,readfds,max_FD。把server放入fds中
max_fd =server;
FD_ZERO(&fds);
FD_ZERO(&readfds);
FD_SET(server,&fds);
while (1)
{
//遍历文件描述符,把fd放入readfds中
for(int fd=0;fd<1024;fd++)
{
if(FD_ISSET(fd,&fds))
{
FD_SET(fd,&readfds);
if(fd>max_fd)max_fd=fd;
}
}
//建立模型连接可读文件
int n=select(max_fd+1,&readfds,NULL,NULL,0);
if(n<0)
{
perror("select");
_exit(EXIT_FAILURE);
}
//遍历发现改变的
for(int fd=0;fd<=max_fd;fd++)
{
if(FD_ISSET(fd,&readfds))
{
//接收新的连接
if(fd == server)
{
int newsock = accept(server,NULL,NULL);
FD_SET(newsock,&fds);
printf("has a new link\n");
}
else{
memset(buf,0,sizeof(buf));
if(read(fd,buf,1024)>0)
{
printf("%s\n",buf);
write(fd,"recved\n",8);
}
else{
printf("recv a close\n");
close(fd);
FD_CLR(fd,&fds);
FD_CLR(fd,&fds); //理论上不清空这个,但莫名会有bug
max_fd=0;
}
}
}
}
}
}
2.UDP通信
2.1什么是UDP通信
UDP是一种无连接的、不可靠的传输层协议。常用于需要快速传输而不需要可靠性的场景。UDP的特点是数据报文的发送和接收是独立的,每个数据报文都包含了源地址和目的地址,并不会因传输问题而重新传输或确认接收
2.2UDP通信的基本步骤
2.2.1服务端
1)创建UDP套接字:使用socket函数
2)绑定地址和端口:使用bind
3)接收数据:recvfrom函数接收客户端发送的数据报文
4)发送数据:sento函数发送数据报文到客户端
2.2.2客户端
1)创建UDP套接字:使用socket函数
2)绑定地址和端口:使用bind(也可以不绑定客户端的地址)
3)设置服务器地址(端口号和ip)
4)接收数据:recvfrom函数接收客户端发送的数据报文
5)发送数据:sento函数发送数据报文到客户端
2.3函数原型
recvfrom函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数信息
sockfd
:要接收数据的套接字描述符,通常是使用socket()
创建的套接字。buf
:用于存放接收到的数据的缓冲区指针。len
:缓冲区的长度,即可以接收的数据的最大字节数。flags
:通常设置为 0,可以使用其他标志来改变函数的行为(如MSG_PEEK
查看数据但不移除它)。src_addr
:指向sockaddr
结构的指针,用于存放发送数据的源地址信息。如果不需要源地址信息,可以传入NULL
。addrlen
:指向socklen_t
类型变量的指针,用于存储src_addr
的长度。如果src_addr
为NULL
,这个参数也可以为NULL
。
返回值成功时返回收到的字节数,失败时返回-1并设置errno
sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明
sockfd
:要发送数据的套接字描述符,通常是使用socket()
创建的套接字。buf
:指向包含要发送的数据的缓冲区的指针。len
:要发送的数据的字节数。flags
:通常设置为 0,可以使用其他标志来改变函数的行为。dest_addr
:指向sockaddr
结构的指针,指定目标地址。addrlen
:dest_addr
结构的长度。
返回值
- 成功时,返回实际发送的字节数。
- 失败时,返回
-1
,并设置errno
。
2.4代码示例
2.4.1服务端代码
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <signal.h>
//宏定义服务端端口
#define PORT 8080
int server;
//ctrl+C关闭终端释放服务器
void handle_signal(int sig)
{
if(sig==SIGINT)
{
printf("\nServer shutting down...\n");
close(server); // 关闭服务器套接字
exit(0);
}
}
int main()
{
struct sockaddr_in host_addr,colient_addr;
socklen_t len =sizeof(colient_addr);
//接收CTRL+C信号
signal(SIGINT,handle_signal);
//生成套接字
server=socket(AF_INET,SOCK_DGRAM ,0);
if(server<0)
{
perror("socket");
_exit(EXIT_FAILURE);
}
//绑定服务端地址与端口
host_addr.sin_family=AF_INET;
host_addr.sin_port=htons(PORT);
host_addr.sin_addr.s_addr=INADDR_ANY;
if(bind(server,(struct sockaddr *)&host_addr,sizeof(host_addr))<0)
{
perror("bind");
_exit(EXIT_FAILURE);
}
while (1)
{
//清空输入数组,将接收的写入其中然后打印
char rxbuf[100];
memset(rxbuf,0,sizeof(rxbuf));
int n = recvfrom(server,rxbuf,sizeof(rxbuf),0,(struct sockaddr*)&colient_addr,&len);
if(n<0)
{
perror("recvfrom");
_exit(EXIT_FAILURE);
}
printf("%s\n",rxbuf);
//接收到后向客户端发送ok
if(n>0)
{
int m= sendto(server,"ok\n",7,0,(struct sockaddr*)&colient_addr,len);
if(m<0)
{
perror("sendto");
_exit(EXIT_FAILURE);
}
}
}
close(server);
}
2.4.2客户端代码
colient.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <signal.h>
#include <arpa/inet.h>
//宏定义服务端IP与客户端端口(客户端端口不能与服务端相同)
#define PORT 8081
#define IP "192.168.137.138"
int main()
{
int server;
struct sockaddr_in host_addr,colient_addr;
socklen_t len=sizeof(host_addr);
//创建套接字
if((server=socket(AF_INET,SOCK_DGRAM,0))<0)
{
perror("socket");
_exit(EXIT_FAILURE);
}
//绑定客户端地址(也可以不绑定)
colient_addr.sin_family=AF_INET;
colient_addr.sin_port=htons(PORT);
colient_addr.sin_addr.s_addr=INADDR_ANY;
bind(server,(struct sockaddr*)&colient_addr,sizeof(colient_addr));
//设置服务端地址与端口(需要直到和谁通信)
host_addr.sin_family=AF_INET;
host_addr.sin_port=htons(8080);
host_addr.sin_addr.s_addr=inet_addr(IP);
while (1)
{
char rxbuf[100],txbuf[100];
//清空输出数组,并输入新的信息
memset(txbuf,0,sizeof(txbuf));
printf("请输入你要发送的信息:");
scanf("%s",txbuf);
//发送信息给服务端
if(sendto(server,txbuf,sizeof(txbuf),0,(struct sockaddr*)&host_addr,len)<0)
{
perror("sendto");
_exit(EXIT_FAILURE);
}
//清空输入数组,将接收的写入其中然后打印
memset(rxbuf,0,sizeof(rxbuf));
if(recvfrom(server,rxbuf,128,0,(struct sockaddr*)&host_addr,&len)<0)
{
perror("recvfrom");
_exit(EXIT_FAILURE);
}
printf("%s",rxbuf);
}
}