前两篇文章分别讲了socket的编程基础和IO多路复用漫谈(附epoll示例),本篇文章基于libev开发了一个聊天室(chat room)应用,包括服务端和客户端。作为对比,同时给出了多线程版本的实现。
聊天室应用
一个聊天室(chat room)类似于微信里的group,某个人发送的消息会以广播的形式发送给所有其他在线的人。在这个示例中,服务端与客户端通过TCP长连接的方式通信,服务端使用与客户端通信的socket的值唯一标识一个用户。
下面分别给出了使用libev和pthread编写的聊天室示例,对比事件驱动和多线程这两种网络编程模式。
基于libev的聊天室
使用libev
libev的文档位于作者的网站上http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod,需要翻墙才能打开,作为替代方案可以直接使用man libev
查看。
使用libev包括以下几步:
源码
注意,client端的代码在前,server端的代码在后。github的gist是按文件名排序的。
服务端
服务端通过一个全局数组clients记录所有在线的用户,当有客户端连接时,accept返回一个新的socket,将这个socket作为数组的索引,保存客户端的ip端口对。当用户下线后,将这个元素清空(置为NULL)。clients的大小限制了同时在线的人数,因为accept返回的socket是唯一的,其次这个值总是从可用socket的最小的值开始递增,除非增加到了最大值(之后又从可用的最小值开始递增)。
服务端使用libev注册了两个事件,分别是server socket的可读事件和client socket的可读事件。server socket可读时调用accept接受新的请求,返回一个新的socket,然后创建一个新的watcher注册这个socket的可读事件。当client socket可读时读取用户发送过来的消息并广播到其他在线的用户。这里使用了一种简单巧妙的方式进行广播,根据当前在线的人数遍历clients数组,跳过自己并过滤掉为NULL的元素,元素的下标就是连接那个用户的socket。注意,当用户下线后,使用ev_io_stop
停止对应的watcher。
客户端
为了使用户能同时发送和接收数据,同样使用libev注册了两个事件,分别是socket可读和标准输入可读事件。当标准输入可读时说明用户输入了内容,调用send发送数据。当socket可读时调用recv接收数据并打印出来。
编译运行
上面的程序依赖libev库,所以需要先安装libev,比如CentOS上执行命令yum install libev
。使用GCC编译的时候需要指定-lev
选项。因为linux上的库一般都是lib开头,-lxx
会自动扩展为libxx。
编译的命令如下:
gcc libev_server.c -o libev_server -lev
gcc libev_client.c -o libev_client -lev
启动服务端监听1111端口
> ./libev_server 1111
在两个shell窗口中启动客户端
> ./libev_client 127.0.0.1 1111
使用其中一个发送消息,输入hello
> ./libev_client 127.0.0.1 1111
connected successfully!
hello
服务端的输出如下:
> ./libev_server 1111
Connected with 127.0.0.1:50554
1 client(s) online.
Connected with 127.0.0.1:50556
2 client(s) online.
message: hello(127.0.0.1:50554)
使用Ctrl+C可以分别结束服务端和客户端。
使用nc作为客户端
其实可以使用号称网络工具中的瑞士军刀netcat(即nc)代替上面的客户端,nc使我们能够同时发送和接收数据,我在调试服务端程序的时候就是用nc。
启动服务端后在另一个shell中执行:
nc 127.0.0.1 1111
多线程版本的聊天室
源码
因为使用了POSIX thread,gcc编译的时候需要指定-lpthread
选项。
服务端
服务端的主线程在一个循环里接受请求,没有请求时,它会被accept函数阻塞。
当有客户端建立连接时,创建一个子线程负责与客户端通信,子线程同样在一个循环里接收并广播消息,当没有新的数据时,对recv的调用会被阻塞。
如果有多个客户端同时发送消息,则有可能出现多个线程对同一个socket调用send函数。这是安全的,POSIX的send/recv
是原子操作。如果有两个线程同时调用send函数,第二个线程会被阻塞直到第一个线程完成,这个过程很快,因为send仅仅是把数据写到发送缓存。如果使用的socket类型是SOCK_STREAM,则send/recv系统调用可能只发送或接收了部分数据,这会导致数据被截断,比如线程1发送hello,线程2发送world,接收端可能recv返回两次,第一次是hellowo,第二次是rld。
客户端
为了能够同时发送和接收数据,客户端同样使用了多线程,在主线程里建立连接后,启动子线程接收数据,主线程则读取用户的输入,并发送给服务端。当用户输入quit
,主线程退出后,进程结束,子线程当然也结束了。同样的,当子线程发现连接断开了,则使用exit(0)
退出进程,否则主线程会一直等待用户输入。