源码在最下方
TCP服务器
- 并发服务器
- 一请求一线程 (已不被推崇)
- IO 多路复用, epoll
- TCP服务器百万级连接
一请求一线程
main()
申请一个 int sockfd = socket(AF_INET, SOCK_STREAM, 0);
初始化一个实例 sockaddr_in
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
add.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
bind();
将一本地地址与一套接字捆绑。本函数适用于未连接的数据报或流类套接字,在
connect()
或listen()
调用前使用。当用socket()
创建套接字后,它便存在于一个名字空间(地址族)中,但并未赋名。bind()
函数通过给一个未命名套接字分配一个本地名字来为套接字建立本地捆绑(主机地址/端口号)。
bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in))
listen();
创建一个套接字并监听申请的连接
#include <sys/socket.h> int listen(int sockfd, int backlog); // sockfd: 用于标识一个已捆包未连接套接字的文件描述符 // backlog: 等待连接队列的最大长度
accept()
;
accept()
是在一个套接字接受的一个连接。accept()
是c语言中网络编程的重要的函数,本函数从s的等待连接队列中抽取第一个连接,创建一个与s同类的新的套接字并返回句柄。
单纯的 sockfd
无法解决多个客户端连接时,如何分辨的问题
可通过应用层协议来解决
但随着客户端你的增多 – 如:100W
不适合用一请求一线程的方式来处理。
利用 epoll
epoll 接口
-
int epoll_create(int size);
创建一个
epoll
的句柄,size
用来告诉内核这个监听的数目一共有多大。这个参数不同于select()
中的第一个参数,给出最大监听的fd+1
的值。需要注意的是,当创建好epoll
句柄后,它就是会占用一个fd
值,在linux
下如果查看/proc/
进程id/fd/
,是能够看到这个fd
的,所以在使用完epoll
后,必须调用close()
关闭,否则可能导致fd被耗尽。 -
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll
的事件注册函数,它不同与select()
是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()
的返回值,第二个参数表示动作,用三个宏来表示:-
EPOLL_CTL_ADD:注册新的fd到epfd中;
-
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
-
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的
fd
,第四个参数是告诉内核需要监听什么事,struct epoll_event
结构如下:
-
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events
可以是以下几个宏的集合:
- EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
- EPOLLOUT:表示对应的文件描述符可以写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
边沿触发vs水平触发
epoll事件有两种模型,边沿触发:edge-triggered (ET), 水平触发:level-triggered (LT)
水平触发(level-triggered)
socket接收缓冲区不为空 有数据可读 读事件一直触发
socket发送缓冲区不满 可以继续写入数据 写事件一直触发
边沿触发(edge-triggered)
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
边沿触发仅触发一次,水平触发会一直触发。
-
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
类似于
select()
调用。参数events
用来从内核得到事件的集合,maxevents
告之内核这个events
有多大,这个maxevents
的值不能大于创建epoll_create()
时的size
,参数timeout
是超时时间(毫秒,0 会立即返回,-1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0
表示已超时。
- 第1个参数 epfd是 epoll的描述符。
- 第2个参数 events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。
- 第3个参数 maxevents表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。
- 第4个参数 timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。
$ gcc -o tcp_server tcp_server.c
$ ./tcp_server 8888
需要一个 NetAssist.exe
的软件可以做测试