一、需求分析
聊天室中如果有人说话,服务器将内容传送给聊天室的其他人。
那么就需要客户端和服务端两个程序,一个人发送一个消息,服务器向所有人发送一遍消息,所有人的客户端接收消息,也就是说客户端负责发送和接受消息,服务端负责接收和转发消息。
1.客户端Client:
可以主动连接服务端;
可以与服务器之间完成接收和发送消息;
2.服务端Server:
可以接受来自客户端的连接请求;
将客户端发来的信息发送给对应的客户(广播或者私聊);
二、实现逻辑
1、服务端Server
创建服务器套接字:socket
绑定本机IP和端口:bind
监听客户端:listen
等待客户端连接:accept
发送消息:send
接收消息:recv
1)创建套接字socket
sockfd = socket(PF_INET,SOCK_STREAM,0);
2)使用bind() 将套接字与本IP和某一端口绑定
//绑定端口号和IP
serverAddr.sin_family = PF_INET;
serverAddr.sin_port = htons(SERVER_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);bind(sockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
3)监听套接字listen()
int ret = listen(sockfd,5);
4)接受客户端的连接请求 accept(),返回一个对此连接的新套接字
int clientfd = accept( sockfd,(struct sockaddr*)&client_address,&client_addrLenth);
sockfd:套接字描述符,
client_address:提出连接请求的客户端的主机地址
client_addrLenth:客户端的地址长度
5)用新的套接字与客户端通信,发送send() 和接受recv() 客户端数据
int send(int sockfd, const void * data, int data_len, unsigned int flags)
sockfd:套接字描述符
data:指向要发送数据的指针
data_len:数据长度,flags:通常为0
6)关闭套接字close()
2、客户端Client
客户端的端口号是系统自动分配的,所以客户端并不需要 bind 绑定地址,而且也不需要设置监听的套接字,因此也不需要 listen。
客户端在用 socket 创建套接字后直接调用 connect 向服务器发起连接即可,connect 函数通知 Linux 内核完成 TCP 三次握手连接,最后把连接的结果作为返回值。成功建立连接后我们就可以调用 send 和 recv 来发送数据、接收数据,最后调用 close 来断开连接释放资源。
创建客户端套接字:socket
向服务器发起连接:connect
发送消息:send
接收消息:recv
三、提升服务器的处理能力?
服务器需要维护与多个客户端的连接。对于一个服务器,要是聊天的人一多就会出现严重延迟是绝对不可以的,也就是一个个轮询的方式是费时费力的,那么我们会想办法解决这个问题。
1、为什么用epoll?
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select/poll的实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
epoll的底层维护是一颗红黑树,查找和删除修改等等操作都是log级别的,所有很快,具体来说就是一颗红黑树,里面有很多fd,此时来了一个事件,我在树上快速查找有没有与之对应的fd,有就将其添加至list里。然后由下面讲的epoll_wait去等,等待list不为空、收到信号、超时这三种条件后返回一个值。
2、epoll的接口
epoll的实现非常简单,一共就三个接口函数:
-
int epfd = epoll_create(int size);
首先通过create_epoll(int size)来创建一个epoll的句柄,其中size为要监听的数目。这个函数会返回一个新的epoll句柄 epfd ,之后的所有操作将通过 epfd 来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。 -
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,第四个参数是告诉内核需要监听什么事, -
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,通过调用收集在句柄为epfd的epoll中监控到的已发生的事件,把这些事件放入事件队列events里面去。
3、关于LT和ET两种工作模式:
对于水平触发(LT) 模式,一个事件只要有,就会一直触发。
对于边缘触发(ET) 模式,在一个事件从无到有时才会触发。
水平触发是每次epoll_wait() 会将所有可读写的fd返回,系统开销比较大,而边沿触发则是只会返回一次,如果这次我们没有及时处理,那么下一次调用epoll_wait() 则不会有这个fd,除非这个fd再次被触发事件。
对于socket的读事件
水平模式——只要在socket上有未读完的数据,就会一直产生EPOLLIN事件;
于边缘模式——socket上每新来一次数据就会触发一次,如果上一次触发后未将socket上的数据读完,也不会再触发,除非再新来一次数据。
对于socket写事件
水平模式——如果socket的TCP窗口一直不饱和,就会一直触发EPOLLOUT事件;
边缘模式——只会触发一次,除非TCP窗口由不饱和变成饱和再一次变成不饱和,才会再次触发EPOLLOUT事件。
结论:
ET模式仅当状态发生变化的时候才获得通知,这里说的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;
而LT模式是只要有数据没有处理就会一直通知下去的.