一,socket通信的过程

二,相关函数及结构
1,socket函数
// 用于创建流式套接字
int socket(int domain, int type, int protocol)
// 如果成功,返回一个非负的文件描述符,表示新创建的套接字。
// 如果失败,返回 -1,并设置 errno 来指示错误的原因。
| 参数 | 解释 |
|---|---|
| domain | 要创建的sockfd的协议族,(AF_INET:IPv4协议族)、(AF_INET6:IPv6协议族) |
| type | 套接字类型(SOCK_STREAM:字节流套接字)、(SOCK_DGRAM:数据报套接字) |
| protocol | 协议类型(可以直接填写0,使用默认协议),(如果是TCP协议的话就填写IPPROTO_TCP,UDP和SCTP协议类似) |
2,sockaddr_in结构
// 用于设置套接字
struct sockaddr {
unsigned short sa_family; // 2 bytes 地址族, AF_xxx
char sa_data[14];// 14 bytes 协议地址
};
struct sockaddr_in {
short sin_family; // 2 bytes 地址族,通常设置为AF_INET表示IPv4协议
unsigned short sin_port; // 2 bytes 端口号,以网络字节序表示
struct in_addr sin_addr; // 4 bytes IP地址,以网络字节序表示
char sin_zero[8]; // 8 bytes 填充字段,通常设置为0
};
struct in_addr {
unsigned long s_addr; // 4 bytes 使用inet_pton()加载
};
程序员不应操作sockaddr,sockaddr是给操作系统用的。程序员把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数。
3,bind函数
// 用于将参数sockfd和addr绑定在一起
// 使sockfd这个文件描述符监听addr所描述的地址和端口号
int bind (int sockfd, const struct sockaddr * addr, socklen_t addrlen);
// 成功返回0,失败返回-1, 设置errno
| 参数 | 解释 |
|---|---|
| sockfd | 套接字描述符 |
| addr | 套接字地址结构体 |
| addrlen | 套接字地址结构体的长度 |
4,listen函数
// 用于将套接字从CLOSE状态转换为LISTEN状态
int listen(int sockfd, int backlog);
// 成功则返回0,失败返回-1,错误原因存于errno中
| 参数 | 解释 |
|---|---|
| sockfd | 套接字文件描述符 |
| backlog | 指定允许在等待连接队列中排队的最大连接数量 |
5,accept函数
// 用于跟客户端建立连接,并返回客户端套接字
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 成功返回一个新的socket文件描述符,用于和客户端通信,
// 失败返回-1,设置errno
| 参数 | 解释 |
|---|---|
| sockfd | 监听套接字的文件描述符 |
| addr | 指向用于存储客户端地址的结构体的指针 |
| addrlen | 指向addr结构体大小的指针,用于接收客户端地址结构体的实际大小 |
6,connect函数
// 用于客户端建立tcp连接
int connect (int sockfd,struct sockaddr * addr,int addrlen);
// 成功则返回0,失败返回-1,错误原因存于errno中
| 参数 | 解释 |
|---|---|
| sockfd | 套接字文件描述符 |
| addr | 指向目标服务器地址的结构体指针 |
| addrlen | addr结构体的大小 |
7,recv与read函数
// 用于接受信息数据
int recv(int sockfd, void *buf, size_t len, int flags);
// 如果成功接收到数据,返回实际接收到的数据的字节数。
// 如果连接关闭,返回0。
// 如果发生错误,返回-1,并设置errno以指示错误的原因。
| 参数 | 解释 |
|---|---|
| sockfd | 套接字文件描述符 |
| buf | 接收数据的缓冲区 |
| len | 要接收的最大字节数 |
| flags | 函数调用的标志 |
// 用于接受信息数据
ssize_t read(int fd, void *buf, size_t count);
// 如果成功接收到数据,返回实际接收到的字节数。
// 如果连接关闭,返回0。
// 如果发生错误,返回-1,并设置errno以指示错误的原因。
| 参数 | 解释 |
|---|---|
| fd | 文件描述符,可以是套接字文件描述符 |
| buf | 接收数据的缓冲区 |
| count | 要读取的最大字节数 |
8,send与write函数
// 用于发送信息数据
int send(int sockfd, const void *buf, size_t len, int flags);
// 如果成功发送数据,返回实际发送的字节数。
// 如果连接关闭,返回-1。
// 如果发生错误,返回-1,并设置errno以指示错误的原因。
| 参数 | 解释 |
|---|---|
| sockfd | 套接字文件描述符 |
| buf | 是要发送的数据 |
| len | 要发送的字节数 |
| flags | 函数调用的标志 |
// 用于发送信息数据
ssize_t write(int fd, const void *buf, size_t count);
// 如果成功写入数据,返回实际写入的字节数。
// 如果发生错误,返回-1,并设置errno以指示错误的原因。
| 参数 | 解释 |
|---|---|
| fd | 文件描述符,可以是套接字文件描述符 |
| buf | 要发送的数据 |
| count | 要写入的字节数 |
9,close函数
// 关闭套接字
int close(int fd)
// 若close成功则返回0,否则返回-1并置errno
三,socket通信的简单实现
1,服务端server.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#define PORT 9990 // 端口号
#define SIZE 1024 // 定义的缓冲区大小
int main()
{
// 创建套接字
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1)
{
perror("socket");
return -1;
}
// 设置套接字属性
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; // Internet地址族
addr.sin_port = htons(PORT); // 端口号
addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
// 绑定套接字到地址和端口
int ret = bind(server_socket, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
return -1;
}
// 将socket设置为监听模式
ret = listen(server_socket, 5);
if (ret == -1)
{
perror("listen");
return -1;
}
struct sockaddr_in cliaddr;
int addrlen = sizeof(cliaddr);
std::cout << "正在等待客户端连接......" << std::endl;
// 创建一个和客户端交流的套接字,用于接受客户端的连接
int client_socket = accept(server_socket, (struct sockaddr *)&cliaddr, (socklen_t *)&addrlen);
if (client_socket == -1)
{
perror("accept");
return -1;
}
// 输出接收到的客户端的ip地址
std::cout << "成功接收到一个客户端:" << inet_ntoa(cliaddr.sin_addr) << std::endl;
char buf[SIZE]; // 接收数据的缓冲区
while (1)
{
// 接收客户端数据
int ret = recv(client_socket, buf, SIZE - 1, 0); // 数据长度
if (ret == -1)
{
perror("recv");
break;
}
if (ret == 0)
{
break;
}
buf[ret] = '\0'; // 断句
for (int i = 0; i < ret; i++)
{
buf[i] = buf[i] + 'A' - 'a';
}
std::cout << "收到客户端发来的消息:" << buf << std::endl;
// 向客户端发送数据
send(client_socket, buf, ret, 0);
// 结束标志
if (strncmp(buf, "END", 3) == 0)
{
break;
}
}
// 关闭socket连接释放资源资源
close(client_socket);
return 0;
}
2,客户端client.cpp
#include<iostream>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#define PORT 9990
#define SIZE 1024
int main()
{
//创建和服务器连接套接字
int client_socket = socket(AF_INET, SOCK_STREAM, 0);
if(client_socket == -1)
{
perror("socket");
return -1;
}
//设置套接字属性
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; //设置Internet地址族
addr.sin_port = htons(PORT); //设置端口号
addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置IP地址
inet_aton("127.0.0.1", &(addr.sin_addr)); //将点分十进制的字符串转换为网络字节序的二进制数
//连接服务端
int listen_socket = connect(client_socket, (struct sockaddr *)&addr, sizeof(addr));
if(listen_socket == -1)
{
perror("connect");
return -1;
}
std::cout<<"成功连接到一个服务端"<<std::endl;
char buf[SIZE] = {0};
while(1) //向服务端发送数据,并接收服务端转换后的大写字母
{
std::cout<<"请输入小写的字符串:";
std::cin>>buf;
//向服务端发送数据
send(client_socket, buf, strlen(buf),0);
//接受服务端返回的数据
int ret = recv(client_socket, buf, strlen(buf),0);
if (ret == -1)
{
perror("recv");
break;
}
std::cout<<"收到服务端返回的消息:"<<buf<<std::endl;
if(strncmp(buf, "END", 3) == 0)//当输入end时客户端退出
{
break;
}
}
//关闭socket连接释放资源资源
close(listen_socket);
return 0;
}
四,加入多线程的网络通信
1,多线程的利弊
一个client去连接连接一个server端,那么如果此时我们使用阻塞模式的话,如果只使用一个线程的话,此时第一个client连接过来了,调用了recv()函数,阻塞在等待消息这里,此时如果第二个客户端连接的话,因为程序一直在recv()这里等着,无法处理这个连接请求。那么如果我们使用多线程机制,对每个客户端都使用一个线程,就可以解决这个问题。可是如果有大量的客户连接的话,服务端就要创建大量的线程,大量的线程创建将会消耗许多资源。
2,pthread_create函数
// 线程的创建
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
// 成功返回0,否则返回错误码
| 参数 | 解释 |
|---|---|
| thread | 指向pthread_t类型的指针,用于接收新创建线程的标识符。pthread_t是一个不透明的线程标识符类型,可以用于操作线程的其他函数 |
| attr | 指向pthread_attr_t类型的指针,用于设置新线程的属性。pthread_attr_t是一个线程属性类型,可以用于设置线程的堆栈大小、调度策略等。如果传递NULL,则使用默认线程属性 |
| start_routine | 一个函数指针,指向线程的入口函数(Thread Entry Point)。该函数将在新线程中执行。它必须具有以下签名:void* functionName(void* arg),其中arg是一个指向线程入口函数的参数的指针,可以向线程传递参数 |
| arg | 一个void类型的指针,用于向线程入口函数传递参数。可以将任意类型的指针转换为void类型进行传递 |
3,完整服务端代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#define PORT 9990 // 端口号
#define SIZE 1024 // 定义的数组大小
const int MAX_CLIENTS = 10; // 定义客户端最大连接数
int client_sockets[MAX_CLIENTS];
int num_clients = 0;
void *handle_client(void *client_socket_ptr)
{
int client_socket = *(int *)client_socket_ptr;
char buffer[SIZE];
int ret;
while ((ret = recv(client_socket, buffer, SIZE, 0)) > 0)
{
buffer[ret] = '\0'; // 断句
for (int i = 0; i < ret; i++)
{
buffer[i] = buffer[i] + 'A' - 'a';
}
std::cout << "收到客户端发来的消息:" << buffer << std::endl;
if (send(client_socket, buffer, strlen(buffer), 0) < 0)
{
std::cerr << "send" << std::endl;
break;
}
}
if (ret == 0)
{
std::cout << "客户端连接已断开" << std::endl;
num_clients--;
}
else
{
std::cerr << "receive" << std::endl;
}
close(client_socket);//关闭socket
pthread_exit(NULL);//关闭该线程
// 没有指定去等待子线程,主线程也会等待子线程执行完毕后,才会最后结束程序
}
int main()
{
// 创建套接字
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1)
{
std::cerr << "socket" << std::endl;
return -1;
}
// 设置套接字属性
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; // Internet地址族
addr.sin_port = htons(PORT); // 端口号
addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
// 绑定套接字到地址和端口
int ret = bind(server_socket, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
std::cerr << "bind" << std::endl;
return -1;
}
// 将socket设置为监听模式
ret = listen(server_socket, 5);
if (ret == -1)
{
std::cerr << "listen" << std::endl;
return -1;
}
struct sockaddr_in cliaddr;
int addrlen = sizeof(cliaddr);
std::cout << "正在等待客户端连接......" << std::endl;
int client_socket;
sockaddr_in client_addr;
while (true)
{
socklen_t client_addr_len = sizeof(client_addr);
client_socket = accept(server_socket, (sockaddr *)&client_addr, &client_addr_len);
if (client_socket < 0)
{
std::cerr << "accept" << std::endl;
return EXIT_FAILURE;
}
std::cout << "成功接收到一个客户端:" << inet_ntoa(cliaddr.sin_addr) << std::endl;
if (num_clients < MAX_CLIENTS)
{
// 将客户端socket添加到数组中
client_sockets[num_clients] = client_socket;
// 创建新线程处理客户端消息
pthread_t tid;
if (pthread_create(&tid, NULL, handle_client, (void *)&client_socket) != 0)
{
std::cerr << "create thread" << std::endl;
return EXIT_FAILURE;
}
num_clients++;
}
else
{
std::cerr << "连接失败,客户端连接数量已达上限......" << std::endl;
close(client_socket);
}
}
// 关闭socket连接释放资源资源
close(server_socket);
return 0;
}
五,阻塞与非阻塞I/O
参考连接:
使用fcntl()函数设置socket为阻塞态或非阻塞态Socket的非阻塞模式_socket 非阻塞设置的四种方法使用fcntl()函数设置socket为阻塞态或非阻塞态fcntl函数的用法总结
1,socket的两种工作模式
阻塞式I/O(Blocking I/O):在进行I/O操作的过程中,如果数据还没有准备好,则会等待数据准备好再进行后面的操作。在等待数据的过程中,线程阻塞,无法进行其他操作。
非阻塞式I/O(Non-blocking I/O):与阻塞式I/O不同,非阻塞式I/O不会在等待数据准备好时阻塞线程。如果数据没有准备好,它会立即返回一个错误代码。线程可以在等待数据准备好的同时继续执行其他操作。
通常情况下:将Socket设置为非阻塞模式更常见和有意义的是在服务器端,而不是在客户端。
在服务器端,当使用多线程或多进程模型时,将Socket设置为非阻塞模式可以提高并发性能。这允许服务器能够同时处理多个客户端连接请求,而不会被阻塞在单个连接的IO操作上。在非阻塞模式下,服务器可以通过调用accept()接受客户端连接请求,如果没有新的连接请求到达,accept()立即返回,不会阻塞等待。这样服务器可以继续处理其他事务或者响应其他客户端的请求。
另一方面,在客户端,将Socket设置为非阻塞模式可能没有那么常见,因为通常客户端更关注单个连接的IO操作。不过,如果客户端在处理多个Socket连接,或者希望在等待数据到达时能同时进行其他操作,可以考虑将其设置为非阻塞模式。
需要注意的是,在非阻塞模式下,IO操作将返回实际读取/写入的字节数或错误码,而不会一直等待直到操作完成。因此,在非阻塞模式下,需要适当地处理返回的值和错误码,进行合适的重试或者等待。同时,也需要使用适当的事件驱动机制(如select、poll、epoll等)来监视和处理Socket的可读、可写事件。
2,设置socket非阻塞的方法
socket在创建的时候默认是阻塞的,将socket设置为非阻塞的有以下几种方法
(1)使用fcntl函数
fcntl函数是一个系统调用,它可以用于对文件描述符进行各种控制操作,包括设置非阻塞。
#include <fcntl.h>
//...
int fd = socket(AF_INET, SOCK_STREAM, 0);
int flag = fcntl(fd,F_GETFL,0);//获取文件fd当前的状态
//int flag = fcntl(fd,F_GETFL);//不用第3个参数也可以
if(flag<0)
{
perror("fcntl F_GETFL fail");
close(fd);
}
if (fcntl(fd, F_SETFL, flag | O_NONBLOCK) < 0)//设置为非阻塞态
{
perror("fcntl F_SETFL fail");
close(fd);
}
// 服务端绑定、监听等;客户端连接等
//...
(2)使用ioctl函数
ioctl函数也可以用来对文件描述符进行控制操作,包括设置非阻塞。
#include <sys/ioctl.h>
//...
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置非阻塞
int flags = 1; //将flags设置为非零值表示启用非阻塞模式
ioctl(sockfd, FIONBIO, &flags); //FIONBIO是一个常量,表示设置或获取非阻塞I/O
// 服务端绑定、监听等;客户端连接等
//...
(3)创建socket时设置
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
(4)accept4
int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);
六,I/O多路复用
参考链接:
1,目前的IO多路复用方案
解决方案总览
Linux: select、poll、epoll
MacOS/FreeBSD: kqueue
Windows/Solaris: IOCP
常见软件的IO多路复用方案
redis: Linux下 epoll(level-triggered),没有epoll用select
nginx: Linux下 epoll(edge-triggered),没有epoll用select
2,select相关函数详解
(1)select
// 用于在一组文件描述符上等待可读、可写或异常事件发生。
// 它使用三个文件描述符集合(readfds、writefds和exceptfds)指定要关注的事件类型。
// nfds参数表示最大的文件描述符值加1。timeout参数用于指定超时时间。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// 如果成功,返回就绪事件的文件描述符数。
// 如果超时,返回0。
// 如果出错,返回-1,并设置errno以指示错误的原因。
| 参数 | 解释 |
|---|---|
| nfds | 最大的文件描述符加1 |
| readfds | 关注可读事件的文件描述符集合 |
| writefds | 关注可写事件的文件描述符集合 |
| exceptfds | 关注异常事件的文件描述符集合 |
| timeout | 超时时间,可以是NULL表示阻塞,或者指向struct timeval结构表示具体的超时时间 |
(2)FD_ZERO
用来清空文件描述符组。每次调用select前都需要清空一次
int FD_ZERO(fd_set *fdset); //将一个 fd_set类型变量的所有位都设为0
fd_set writefds;
FD_ZERO(&writefds)
(3)FD_SET、FD_CLR
添加一个文件描述符到组中,FD_CLR对应将一个文件描述符移出组中
int FD_SET(int fd, fd_set *fdset); //将变量的某个位置位(置1)
int FD_CLR(int fd, fd_set *fdset); //清除变量某个位
FD_SET(fd, &writefds);
FD_CLR(fd, &writefds);
(4)FD_ISSET
检测一个文件描述符是否在组中,我们用这个来检测一次select调用之后有哪些文件描述符可以进行IO操作
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位
if (FD_ISSET(fd, &readfds)){
/* fd可读 */
}
(5)FS_SETSIZE
select可同时监听的文件描述符数量是通过FS_SETSIZE来限制的,在Linux系统中,该值为1024,当然我们可以增大这个值,但随着监听的文件描述符数量增加,select的效率会降低。
(6)完整服务端代码
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#define SERV_PORT 8888
int main(int argc, char *argv[])
{
int listenfd, connfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN];
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // SO_REUSEADDR允许重用本地地址端口
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(listenfd, 128);
fd_set rset, allset;
int ret, maxfd = 0;
maxfd = listenfd;
FD_ZERO(&allset); // 集合全部设为0
FD_SET(listenfd, &allset);
while (1)
{
rset = allset;
ret = select(maxfd + 1, &rset, NULL, NULL, NULL); // 返回有事件发生的个数
if (FD_ISSET(listenfd, &rset))
{
clie_addr_len = sizeof(clie_addr);
connfd = accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
std::cout<<"received from "<<inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str))
<<" at PORT "<<ntohs(clie_addr.sin_port)<<std::endl;
FD_SET(connfd, &allset); // 将一个集合中的事件“加入”
if (maxfd < connfd)
maxfd = connfd;
if (ret == 1)
continue; // 说明select只返回一个,并且只是listenfd,无需后续执行
}
int n = 0; // read读到的字节数
for (int i = listenfd + 1; i < maxfd + 1; ++i)
{
if (FD_ISSET(i, &rset)) // 判断文件描述符是否在集合中
{
n = read(i, buf, sizeof(buf));
if (n == 0)
{
close(i);
FD_CLR(i, &allset); // 将一个集合中的事件“拿走”
}
else if (n == -1)
{
}
else
for (int j = 0; j < n; ++j)
buf[j] = toupper(buf[j]);
write(i, buf, n);
//write(STDOUT_FILENO, buf, n);//向屏幕输出
std::cout<<buf<<std::endl;
}
}
}
close(listenfd);
return 0;
}
3,poll相关函数详解
(1)poll
// 用于等待一组文件描述符上的事件发生。
// 它通过struct pollfd结构指定要监视的文件描述符和关注的事件类型。
// 函数将在事件就绪或超时时返回,并将就绪的事件存储在数组fds中。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// 如果成功,返回就绪事件的数量。
// 如果超时,返回0。
// 如果出错,返回-1,并设置errno以指示错误的原因。
| 参数 | 解释 |
|---|---|
| fds | 一个struct pollfd类型的数组,每个元素指定要监视的文件描述符、感兴趣的事件和返回的事件 |
| nfds | 数组中的文件描述符数量 |
| timeout | 超时时间,单位是毫秒 |
用于监视并等待多个文件描述符的属性变化,poll模型是基于select最大文件描述符限制提出的,跟select一样,只是将select使用的三个基于位的文件描述符改为使用一个数组的形式,对于各种可能的事件进行了一个包装。使用poll函数时,没有明确的限制,可以监控的文件描述符数目取决于系统的可用资源。poll函数将所需要监听的fd集合传递给内核,内核直接检测,没有遍历的过程,效率相对较高。适用于数据量大情况
(2)pollfd结构体
struct pollfd {
int fd; //被监视的文件描述符
short events; //指定监测fd的事件(输入、输出、错误),每一个事件有多个取值,如下:
short revents; //文件描述符的操作结果事件,内核在调用返回时设置这个域
};
events 域中请求的任何事件都可能在 revents 域中返回,每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件。
(3)完整服务端代码
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
using namespace std;
#define MAX_CLIENTS 1024
#define BUFFER_SIZE 1024 // 缓冲区大小
int main()
{
// 创建server_fd
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1)
{
perror("Failed to create server_sock");
return -1;
}
// 服务器网络地址结构体
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET; // IPV4
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // INADDR_ANY 为服务器地址为本地所有网卡
// 绑定服务器
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
{
perror("Failed to bind socket");
return -1;
}
// 进行监听
if (listen(server_fd, MAX_CLIENTS) == -1)
{
cerr << "Failed to listen client" << endl;
return -1;
}
cout << "Server listening on: "
<< "127.0.0.1: 8888" << endl;
// poll创建
struct pollfd fds[MAX_CLIENTS + 1]; // pollfd数组,包括服务器套接字和所有客户端套接字
memset(fds, 0, sizeof(fds)); // 初始化
fds[0].fd = server_fd; // 设置第一个元素为服务器套接字
fds[0].events = POLLIN; // 监听POLLIN事件,表示有数据可读
int clients_num = 0; // 客户端数量
while (true)
{
// 调用poll函数,等待事件
if (poll(fds, clients_num + 1, -1) < 0)
{ // nfds:需要监控的文件描述符的数量, -1表示无限等待,直到事件发送
perror("poll error");
break;
}
// 如果服务器套接字上有POLLIN事件,说明有新的客户端连接
if (fds[0].revents & POLLIN)
{ //& 与 两个位都为1时,结果才为1
if (clients_num == MAX_CLIENTS)
{
// 客户端数量超限
cerr << "Too many clients" << endl;
continue;
}
// 接受客户端请求
struct sockaddr_in client_addr = {0};
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd == -1)
{
perror("Error accepting new client");
}
else
{
clients_num++;
// 将新的客户端套接字添加到fds数组中
fds[clients_num].fd = client_fd;
fds[clients_num].events = POLLIN;
printf("Establish a new connection: %s : %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
}
// 遍历已连接的所有客户端套接字
for (int i = 1; i <= clients_num; i++)
{
if (fds[i].revents & POLLIN)
{
// 服务器端发送和接收数据
char send_buffer[BUFFER_SIZE] = "服务器收到确认!";
char recv_buffer[BUFFER_SIZE];
int recv_size = recv(fds[i].fd, recv_buffer, BUFFER_SIZE, 0);
if (recv_size == -1)
{
cerr << "Error receiving from client" << endl;
}
else if (recv_size == 0)
{
// 客户端断开连接
printf("Client disconnected: client_fd: %d\n", fds[i].fd);
close(fds[i].fd); // 关闭套接字
fds[i] = fds[clients_num]; // 将最后一个元素移动到当前位置,当前套接字删除
memset(&fds[clients_num], 0, sizeof(fds[clients_num]));
clients_num--;
}
else
{
// 接收数据处理
cout << "Received message:" << endl
<< recv_buffer << endl;
// 发送确认数据处理
if (send(fds[i].fd, send_buffer, strlen(send_buffer), 0) == -1)
{
cerr << "Failed to sent data to client: client_fd: " << fds[i].fd << endl;
}
}
}
}
}
close(server_fd);
return 0;
}
4,epoll相关函数详解
(1)epoll_create
// 创建一个epoll实例,返回一个epoll文件描述符,可用于后续的epoll操作。
int epoll_create(int size);
// 如果成功,返回epoll文件描述符。
// 如果失败,返回-1,并设置errno以指示错误的原因。
| 参数 | 解释 |
|---|---|
| size | 在内核中创建的epoll实例的大小 |
需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的。在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
(2)epoll_ctl
// 用于向epoll实例中添加、修改或删除感兴趣的事件。
// 它将文件描述符fd与epoll实例(epfd)关联,并指定感兴趣的事件类型和相关的数据。
// 操作类型op决定了具体的操作,例如添加、修改或删除。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
// 如果成功,返回0。
// 如果失败,返回-1,并设置errno以指示错误的原因。
| 参数 | 解释 |
|---|---|
| epfd | 已创建的epoll文件描述符 |
| op | 操作类型,可以是EPOLL_CTL_ADD、EPOLL_CTL_MOD或EPOLL_CTL_DEL,分别用于添加、修改或删除事件 |
| fd | 关联的文件描述符 |
| event | 是一个struct epoll_event类型的指针,用于指定感兴趣的事件和关联的数据 |
epoll的事件注册函数,epoll_ctl向epoll对象中添加、修改或者删除事件,返回0表示成功,否则返回–1,需要根据errno错误码判断错误类型。它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
| op参数的宏 | 解释 |
|---|---|
| EPOLL_CTL_ADD | 注册新的fd到epfd中 |
| EPOLL_CTL_MOD | 修改已经注册的fd的监听事件 |
| EPOLL_CTL_DEL | 从epfd中删除一个fd |
| events参数的宏 | 解释 |
|---|---|
| EPOLLIN | 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) |
| EPOLLOUT | 表示对应的文件描述符可以写 |
| EPOLLPRI | 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来) |
| EPOLLERR | 表示对应的文件描述符发生错误 |
| EPOLLHUP | 表示对应的文件描述符被挂断 |
| EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 |
| EPOLLONESHOT | 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 |
(3)epoll_wait
// 用于阻塞的等待可以执行IO操作的文件描述符直到超时
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
// 返回需要处理的事件数目,
// 如返回0表示已超时,
// 如返回–1,则表示出现错误,需要检查 errno错误码判断错误类型。
| 参数 | 解释 |
|---|---|
| epfd | epoll的描述符 |
| events | 用来从内核得到事件的集合,,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存) |
| maxevents | 表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create() |
| timeout | timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,-1将不确定,也有说法说是永久阻塞 |
(4)完整服务端代码
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
#define MAX_CLIENTS 128
#define BUFFER_SIZE 1024
int main()
{
// 创建服务器socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1)
{
perror("Failed to create socket");
return -1;
}
// 绑定服务器
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
{
cerr << "Failed to bind server_fd" << endl;
close(server_fd);
return -1;
}
// 监听客户端
if (listen(server_fd, MAX_CLIENTS) == -1)
{
perror("listen error");
return -1;
}
cout << "Server started, listening on port: " << 8888 << endl;
// 创建epoll实例
// 变量ev是用于控制添加或修改一个文件描述符的事件类型和数据,events用于存储发生的事件类型和数据。
struct epoll_event ev, events[MAX_CLIENTS];
int epoll_fd = epoll_create(10); // 动态大小的内部事件表,可以初始化为0
if (epoll_fd == -1)
{
perror("epoll_create error");
return -1;
}
// 添加监听套接字到epoll
ev.data.fd = server_fd;
ev.events = EPOLLIN; // 监听套接字的读事件
// 往 epoll 实例中添加一个文件描述符,EPOLL_CTL_ADD是 epoll 操作的一种类型,表示要添加或修改某个文件描述符的事件,
// server_fd是要添加到 epoll 实例中的目标文件描述符,&ev 是要添加的事件数据。
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1)
{
perror("epoll_ctl error");
return -1;
}
while (true)
{
// 等待事件,-1 表示阻塞等待,epoll_fd 是 epoll 实例的文件,events 是一个 struct epoll_event 类型的数组,用于接收已经触发的事件。
int events_count = epoll_wait(epoll_fd, events, MAX_CLIENTS, -1); // epoll_wait 函数的返回值是一个整数,表示有多少个事件已经准备就绪并被返回
if (events_count == -1)
{
perror("epoll_wait error");
break;
}
// 遍历已经就绪的套接字
for (int i = 0; i < events_count; i++)
{
if (events[i].data.fd == server_fd) // 若为监听套接字的事件
{
struct sockaddr_in client_addr = {0};
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd == -1)
{
perror("accept error");
break;
}
// 添加连接套接字到epoll
ev.data.fd = client_fd;
ev.events = EPOLLIN; // 连接套接字的读事件
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1)
{
perror("epoll_ctl client_fd error");
break;
}
printf("Client from: %s: %d connected\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
else if (events[i].events & EPOLLIN) // 若为连接套接字的读事件
{
char recv_buffer[BUFFER_SIZE] = {0};
char send_buffer[BUFFER_SIZE] = "server is recved";//有问题
// 接收数据
int recv_size = recv(events[i].data.fd, recv_buffer, sizeof(recv_buffer), 0);
if (recv_size == -1)
{
perror("recv error");
break;
}
else if (recv_size == 0) // 断开情况下,删除该套接字
{
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr) == -1)
{
perror("epoll_ctl del erroe");
break;
}
// 在删除文件描述符之后,也不需要将 events[i] 结构体初始化,
// 因为它仅用于保存上一次触发的事件相关信息,
// 而已经删除的文件描述符不可能再次触发事件。
close(events[i].data.fd);
printf("Connection closed: socket= %d", events[i].data.fd);
}
else
{
// 判断客户端是否退出
if (strcmp(recv_buffer, "exit") == 0)
{
std::cout << "收到客户端指令,实现客户端退出: socket= " << events[i].data.fd << std::endl;
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr) == -1)
{ // 删除该套接字
perror("epoll_ctl del erroe");
break;
}
close(events[i].data.fd);
printf("Connection closed: socket= %d\n", events[i].data.fd);
continue;
}
printf("Received data: %s\n", recv_buffer);
// 发送数据
if (send(events[i].data.fd, send_buffer, strlen(send_buffer), 0) == -1)
{
cerr << "Failed to send data to client" << endl;
break;
}
}
}
}
}
// 删除 epoll 监听的文件描述符
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, server_fd, nullptr);
close(server_fd);
close(epoll_fd);
return 0;
}
5,select、poll、epoll的区别
(1)接口和数据结构
- select使用fd_set数据结构来存储文件描述符集合。需要在函数调用时传递readfds、writefds和exceptfds参数来指定要监听的文件描述符集合及对应的事件类型。
- poll使用pollfd数据结构来存储文件描述符集合,并通过传递pollfd数组参数来指定要监听的文件描述符集合。
- epoll使用epollevent数据结构存储文件描述符集合,并通过epollctl函数添加到内核事件表中。
(2)文件描述符数量限制
- select和poll都有文件描述符数量的限制,通常最多可监听1024个文件描述符(取决于文件描述符集合的位图长度)。
- epoll没有明确的文件描述符数量限制,能够支持非常大的文件描述符数量。
(3)效率和性能
- select和poll在查找有事件发生的文件描述符时需要遍历整个文件描述符集合,性能较差。
- epoll使用回调机制,将文件描述符添加到内核事件表中,并通过epoll_wait等待内核事件表中有事件发生的文件描述符。在文件描述符数量较多时,epoll相比select和poll拥有更高的性能。
(4)I/O事件触发模式
- select和poll的触发模式只有水平触发(LT,Level Triggered)。
- epoll支持水平触发(LT)和边缘触发(ET,Edge Triggered)两种模式,还可以使用ONE SHOT模式(一个事件只触发一次)。
(5)平台支持
- select和poll在多数操作系统上都有支持,并且是跨平台的。
- epoll是Linux特有的,只能在Linux操作系统上使用。
(6)总结
- select和poll是跨平台的多路复用机制,适用于小规模的并发连接。
- epoll是Linux特有的多路复用机制,适用于大规模的并发连接,具有更好的性能和灵活的事件触发模式支持
本文详细介绍了网络编程中的关键概念,包括使用socketAPI创建和管理套接字,如bind、listen、accept和connect函数。还讨论了多线程在网络通信中的应用,以及如何设置socket为阻塞或非阻塞模式。最后,探讨了I/O多路复用技术,如select、poll和epoll,以及它们在处理并发连接时的优缺点。

6262

被折叠的 条评论
为什么被折叠?



