文章目录
一、一请求一线程
void *client_routine(void *arg) {
int clientfd = *(int *)arg;
while (1) {
char buffer[BUFFER_LENGTH] = {0};
int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (len < 0) {
close(clientfd);
break;
} else if (len == 0) { // disconnect
close(clientfd);
break;
} else {
printf("Recv: %s, %d byte(s)\n", buffer, len);
}
}
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Param Error\n");
return -1;
}
int port = atoi(argv[1]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
perror("bind");
return 2;
}
if (listen(sockfd, 5) < 0) {
perror("listen");
return 3;
}
while (1) {
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len = sizeof(client_addr);
// 请求一次 创建一个线程
int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
// 创建线程
pthread_t thread_id;
pthread_create(&thread_id, NULL, client_routine, &clientfd);
}
return 0;
}
二、socket——IO多路复用模型 epoll
随着客户端越来越多,则不适合使用一请求一线程的方式
首先介绍epoll实现的三个函数:
- epoll_create
- epoll_ctl
- epoll_wait
(1) epoll_create()
#include <sys/epoll.h>
int epoll_create(int size);
- size:从Linux 2.6.8以后就不再使用,但是必须设置一个大于0的值。
- 返回值:调用成功返回一个非负值的文件描述符epollfd,调用失败返回-1。
(2) epoll_ctl()
有了epollfd之后,我们需要将我们需要检测事件的其他fd绑定到这个epollfd上,
或修改一个已经绑定上去的fd的事件类型,或者在不需要时将fd从epollfd上解绑,
这都可以使用epoll_ctl函数。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd:调用epoll_create函数创建的epollfd
- op:操作类型,取值有EPOLL_CTL_ADD、EPOLL_CTL_MOD和EPOLL_CTL_DEL,分别表示向epollfd上添加、修改和移除一个其他fd,当取值是EPOLL_CTL_DEL,第四个参数event忽略不计,可以设置为NULL。
- fd: 被操作的fd
- event: epoll_event结构体的地址
- 返回值:调用成功返回0,调用失败返回-1,可以通过errno错误码获取具体的错误原因.
(3) epoll_event 结构体
struct epoll_event {
uint32_t events; // 需要检测的fd事件,取值与poll函数一样
epoll_data_t data; // 用户自定义数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
事件宏 | 描述 |
---|---|
EPOLLIN | 数据可读 |
EPOLLOUT | 数据可写 |
EPOLLRDHUP | TCP连接被对端关闭,或者关闭了写操作 |
EPOLLPRI | TCP连接被对端关闭,或者关闭了写操作 |
EPOLLRDHUP | 高优先级数据可读,例如 TCP 带外数据 |
EPOLLERR | 错误 |
EPOLLHUP | 挂起 |
EPOLLET | 边缘触发模式 |
EPOLLONESHOT | 最多触发其上注册的事件一次 |
(4) epoll_wait()
创建epollfd,设置好某个fd上需要检测事件并将该fd绑定到epollfd上去后,就可以调用epoll_wait检测事件了。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- epfd: epollfd
- events: 是一个epoll_event结构数组的首地址,这是一个输出参数,函数调用成功后,events中存放的是与就绪事件相关epoll_event结构体数组
- maxevents : 数组元素的个数
- timeout : 超时时间,单位是毫秒,如果设置为0,epoll_wait会立即返回。
- 返回值:调用成功会返回有事件的fd数目;如果返回0表示超时;调用失败返回-1
三、epoll 代码实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define BUFFER_LENGTH 1024
#define EPOLL_SIZE 1024
void *client_routine(void *arg) {
int clientfd = *(int *)arg;
while (1) {
char buffer[BUFFER_LENGTH] = {0};
int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (len < 0) {
close(clientfd);
break;
} else if (len == 0) { // disconnect
close(clientfd);
break;
} else {
printf("Recv: %s, %d byte(s)\n", buffer, len);
}
}
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Param Error\n");
return -1;
}
int port = atoi(argv[1]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
perror("bind");
return 2;
}
if (listen(sockfd, 5) < 0) {
perror("listen");
return 3;
}
int epfd = epoll_create(1);
struct epoll_event events[EPOLL_SIZE] = {0};
struct epoll_event ev;
ev.events = EPOLLIN; // 数据可读
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);// sockfd交给epoll管理
while (1) {
int nready = epoll_wait(epfd, events, EPOLL_SIZE, 5);
if (nready == -1) continue;
int i = 0;
for (i = 0;i < nready;i ++) {
if (events[i].data.fd == sockfd) { // listen
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len = sizeof(client_addr);
int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else {
int clientfd = events[i].data.fd;
char buffer[BUFFER_LENGTH] = {0};
int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (len < 0) {
close(clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
} else if (len == 0) { // disconnect
close(clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
} else {
printf("Recv: %s, %d byte(s)\n", buffer, len);
}
}
}
}
return 0;
}
四、检测IO有没有数据?
- 识别有数据 —— 水平触发
- 监测从无到有的过程 —— 边沿触发
-
LT:(Level_triggered,水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你,如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
-
ET:(Edge_triggered,边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你,这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
五、运行
成功接收到客户端数据