项目目的

练习Linux系统下c++网络编程的相关知识;
实现一个聊天室程序;

项目需求分析

聊天室程序由服务端和客户端组成:

1.服务端:

  • 可以接受来自客户端的连接请求;
  • 将客户端发来的信息发送给对应的客户(广播or私聊);

2.客户端:

  • 可以主动连接服务端;
  • 可以与服务器之间完成接收和发送消息;

实现细节分析

服务端:
1.使用套接字监听端口,接收来自服务器的连接
2.为新的连接建立新的套接字
3.可能存在大量闲置连接,使用epoll,增加并发效率
4.解析客户端发送来的消息,根据解析结果选择发送对象(广播or私聊)
5.客户端断开连接时,对连接相应信息进行清理

客户端:
1.建立与服务端连接的套接字,主动连接服务端
2.使用两个进程,父进程负责与服务端的通信,子进程负责得到用户的输入内容
3.父子进程间使用pipe完成通信
4.断开连接时清理相关信息

通信步骤

TCP服务端:
1.socket()创建TCP套接字
2.使用bind()将套接字与本IP和某一端口绑定
3.使用listen()把套接字设为监听模式,准备接受客户的连接请求
4.accept()接受客户连接请求,返回得到一个对应此连接的新的套接字
5.用得到的新的套接字与客户端进行通信,使用recv()和send()来收发信息
6.断开连接,关闭套接字

TCP客户端:
1.socket()创建TCP套接字
2.使用connect()和刚创建的套接字,连接到服务器
3.与服务器进行通讯,使用recv()和send()收发信息
4.断开连接,关闭套接字

技术准备

socket阻塞与非阻塞

简单的来说,阻塞的方式就是当试图进行读写操作时,如果没有可读或可写的内容,那么就一直等待直到有为止,而非阻塞的方式则是如果没有则立即返回,不会等待。
对于服务器来说,必定要维护与多个客户端的连接,一种传统的方法是我们给每一个socket创建一个线程,采用阻塞的方式进行通信,这种方法很高效,控制起来也很简单,但是仅仅只使用于只有很少的连接的情况,如果连接数比较多,则需要创建大量线程,消耗太多资源。
所以我们需要采用非阻塞的方式,服务端用一个列表存储所有连接套接字的fd,线程数与连接数没有关系,服务器把客户端发来的请求异步地放入函数中交给线程处理,这样一个线程可以处理多个socket连接发来的请求,节约了大量的资源。
socket()函数在创建套接字时,默认是阻塞的,非阻塞的设置方法是:

//将文件描述符设置为非阻塞方式(利用fcntl函数)
fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)| O_NONBLOCK);
epoll

聊天室软件,很有可能出现的场景是有很多人在线,但是并不是人人都时刻在发送信息,如果我们使用select/poll的方式去轮询每一个连接fd,也是一种极大的资源浪费,而epoll的机制完美的适合聊天室这种连接数较多且空闲socket较多的情况。
epoll之所以高效,是因为它将用户关心的文件描述符放入内核里的一个时间列表中,而不是像select/poll那样每次调用都需要重复传入文件描述符或事件集(大量拷贝开销),例如一个事件发生,epoll无需遍历整个被监听的描述符集,而只需要遍历被内核IO事件异步唤醒而加入就绪队列的描述符集合即可。
epoll的两种工作方式——LT(level triggered)水平触发和ET(edge triggered)边沿触发,水平触发是每次epoll_wait会将所有可读写的fd返回,系统开销比较大,而边沿触发则是只会返回一次,如果这次我们没有及时处理,那么下一次调用epoll_wait则不会有这个fd,除非这个fd再次被触发事件。

epoll的使用:

int epoll_create(int size)

创建一个epoll句柄,size是内核监听的数目,也是epoll里支持的最大的句柄数

int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event)

事件注册函数:
epfd是epoll的句柄;
op是代表操作,用三个宏来表示:EPOLL_CTL_ADD注册新的fd到epoll、EPOLL_CTL_MOD修改已经注册过的fd的监听事件、EPOLL_CTL_DEL代表从epoll中删除该fd;
fd代表监听的标识符;
参数结构体epoll_event告诉内核需要监听的事件;

int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout)

等待事件的产生,通过调用收集在句柄为epfd的epoll中监控到的已发生的事件,把这些事件放入事件队列events里面去。
关于epoll使用的更多详解,可以看这篇博客

关于服务端的epoll使用步骤如下:
1.调用epoll_create在Linux内核里创建一个事件表
2.将监听套接字listener的fd注册到事件表中
3.在主循环中,调用epoll_wait来返回就绪的文件描述符队列
4.分别处理该队列中的每一项事件:
- 如果是请求新连接则将新连接的套接字加入到epoll事件表
- 如果是消息发送则调用发送消息的函数

关于客户端的epoll使用步骤如下:
1.调用epoll_create在Linux内核里创建一个事件表
2.将与服务端连接的socket的fd注册到事件表中
3.将父进程从pipe中读取信息的fd注册到事件表中
4.在主循环中,调用epoll_wait来返回就绪的文件描述符队列
5.分别处理该队列中的每一项事件:
- 如果是服务器发来的信息,则读取打印
- 如果是pipe读入了信息,则发送给服务器

关于该聊天室程序的完整代码实现可以去看我的github仓库

实现截图:
在这里插入图片描述


最后说一下我在实现代码时想到的一个问题,即服务器使用accept函数接收了一个来自客户端的connect请求后,返回的新的socket是否会占用一个新的端口呢?
答案是否定的,首先我们从常识上来想,如果一个新的socket就要占用一个新的端口,那么端口数量明显不够用,那么计算机是怎么区分从同一个端口发来的信息应该属于哪个socket呢?其实是靠socket的五元组,即目的主机的IP和端口,本机IP和端口以及协议,有了这些很容易就把从同一个端口传输的不同发往socket的信息区分开了。

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐