看完黑马的课后,感觉他在c语言下实现的示例服务器性能不是很好,所以打算使用stl的set容器来存储文件描述符(Socket客户端),学习了select之后也是动手练习搭建一个简单的本地聊天室服务器。
话不多说直接上代码。
服务器代码:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <set>
using namespace std;
#define SERV_PORT 5555
set<int> connectset;
void error_exit(const char* str) //错误处理函数
{
perror(str);
exit(1);
}
void WriteMessageIn(const int fd, const string& str) //写入发送的信息到其他的客户端中
{
const char* buf = str.c_str();
cout << buf;
for (auto& i : connectset)
{
if (i != fd)
{
write(i, buf, strlen(buf));
}
}
return;
}
int main()
{
int listenfd;
int ret;
//声明套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
error_exit("socket error");
int optval = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); //设置端口复用
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
ret = bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret == -1)
error_exit("bind error");
listen(listenfd, 128);
cout << "Server Started." << endl;
//设置监听集合,rset指读取的集合,allset三全局集合
fd_set rset, allset;
int maxfd = listenfd;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
while (1)
{
rset = allset;
ret = select(maxfd + 1, &rset, NULL, NULL, NULL); // select语句设置IO多路转接,最后一个参数为NULL表示阻塞。
if (ret < 0)
{
error_exit("select error");
}
else if (ret > 0)
{
if (FD_ISSET(listenfd, &rset)) //解决连接服务器的请求
{
int fd = accept(listenfd, NULL, NULL);
if (fd == -1)
error_exit("accept error");
connectset.insert(fd);
FD_SET(fd, &allset);
maxfd = max(fd, maxfd);
string s = "Client Connected: " + to_string(fd) + '\n';
WriteMessageIn(-1, s);
if (--ret == 0)
continue;
}
for (auto& i : connectset)
{
if (FD_ISSET(i, &rset))
{
char buf[BUFSIZ];
int siz = read(i, buf, BUFSIZ);
// cout << "siz:" << siz << endl;
if (siz > 0) //处理有输入的情况
{ // string(buf).substr(0, siz):这里不止为何,若不取子串单单传入buf会导致发送信息混乱的bug
/*这个问题的原因在于,当 read 函数从套接字读取数据时,它不会自动添加空字符('\0')来标记字符串的结尾。因此,如果直接将 buf
* 作为 C 风格字符串传递给 write 函数,write
* 函数会一直读取直到遇到第一个空字符,这可能会导致它读取到缓冲区之外的内存,从而产生未定义行为,包括发送错误的数据。*/
WriteMessageIn(i, "User " + to_string(i) + " :" + string(buf).substr(0, siz) + '\n');
}
else if (siz == 0)
{
FD_CLR(i, &allset);
connectset.erase(i);
close(i);
string s = "Client DisConnected: " + to_string(i) + '\n';
WriteMessageIn(i, s);
}
else
{
error_exit("read error");
}
if (--ret == 0)
break;
}
}
}
}
close(listenfd);
cout << "Server Exited." << endl;
return 0;
}
代码运行截图:
因为这个运行效果和我上一篇文章的练习效果差不多,所以直接把图片拿来用了。
代码使用方法:
首先使用g++进行编译,接着启动服务器。由于该程序比较简单就没有设置客户端的代码,这里在linux下打开任意终端后,输入下述指令来连接本地服务器。
nc 127.0.0.1 5555
nc是netcat的意思,后面的127.0.0.1是本地的虚拟地址,在后面的5555是服务器的端口号,这样就可以连上服务器。
连上服务器后就可以在终端里打字说话,其他在服务器的用户可以收到你的信息。
接着如果需要断开与服务器的连接,在控制台输入 ctrl+c 来终止程序,或者直接关闭终端,这样就可以断开服务器了。
代码分析:
这段代码是一个简单的多客户端服务器应用程序,使用 C++ 和 POSIX 套接字编程。服务器能够接受多个客户端的连接,并将消息广播给所有连接的客户端。以下是代码的主要思路和工作流程:
-
初始化和设置套接字:
- 创建一个 TCP 套接字。
- 设置套接字选项
SO_REUSEADDR
,允许服务器在端口还在TIME_WAIT
状态时重新绑定端口。 - 绑定套接字到服务器的地址和端口上。
- 监听连接请求。
-
服务器主循环:
- 使用
select()
函数等待任何套接字上的活动(包括监听套接字和已连接的客户端套接字)。 - 如果监听套接字上有活动(新连接请求),接受连接并将新的客户端套接字添加到套接字集合中。
- 如果客户端套接字上有活动(数据到达),读取数据并将其广播给所有其他客户端。
- 使用
-
处理新连接:
- 当
select()
检测到监听套接字有连接请求时,服务器调用accept()
接受连接。 - 新的客户端套接字被添加到套接字集合中,以便它可以被
select()
监视。 - 服务器向所有客户端发送一条消息,通知它们有新的客户端连接。
- 当
-
读取和广播消息:
- 对于每个有数据到达的客户端套接字,服务器读取数据。
- 服务器将读取的数据加上前缀(包括客户端的描述和消息内容),然后使用
WriteMessageIn
函数将消息广播给所有其他客户端。
-
处理客户端断开连接:
- 如果
read()
返回 0,表示客户端已经关闭了连接。 - 服务器将关闭的客户端从套接字集合中移除,并关闭该客户端套接字。
- 如果
-
错误处理:
- 如果在任何网络操作中发生错误(如
socket()
、bind()
、accept()
、read()
等),服务器将调用error_exit()
函数,打印错误信息并退出。
- 如果在任何网络操作中发生错误(如
-
消息发送:
WriteMessageIn
函数负责将消息发送给指定的客户端(除了发送者自己)。- 它通过遍历
connectset
集合,使用write()
函数将消息发送给每个客户端。
-
关闭服务器:
- 当服务器退出主循环时(例如,由于错误或管理员干预),它会关闭监听套接字并退出。
代码中的关键点是 select()
函数的使用,它允许服务器同时监视多个套接字的活动,这是实现非阻塞 I/O 和处理多个客户端连接的关键技术。此外,代码中的 WriteMessageIn
函数确保了消息的正确广播,并且通过 cout
将消息打印到服务器的控制台,以便进行调试和监控。