概述
1,写这个东西的初衷
主要还是为了熟练熟练网络编程的东西,这些天有时间将网络编程总细节一点的地方又看了看,主要是一些网络编程中的库函数:setsockopt、TCP、UDP这些东西。
之前刚刚看网络编程方面知识的时候在windows上写了个很简单的网络传输的小程序(就是这个),那个时候就想着能不能写一个聊天室类的程序。刚好这些天看书时候书上有一个聊天室的源代码程序,正好这些天也比较闲,将源码看了一遍后感觉实现起来逻辑还是比较清晰的,于是就动手吧,多敲敲找找感觉也没错。
2、实现功能
运行服务器端,等待客户端连接。
当客户成功连接服务器后可以向服务器发送数据,服务器接收到数据后将接收到的数据发送给其他连接上的客户端。
客户端连接后发送*+name,可以设置自己在其他客户端上输出时的名字
3、一些细节
(1)在服务器端,设计一个结构体用来存放客户端的信息
struct client_data
{
sockaddr_in address; //客户端的地址
char my_name[MAX_NAME_LEN]; //用于保存客户端的名字
char *write_buf; //这里存放的是将要发送给其他客户端的数据
char buf[BUFFER_SIZE]; //这里存放的是从客户端接收到的数据
};
(2)I/O复用采用poll模型
监听着10个pollfd结构体,下标0作为listenfd
pollfd fds[MAX_USER_NO + 1];
/*监听文件描述符*/
fds[0].fd = lfd;
fds[0].events = POLLIN | POLLERR;
fds[0].revents = 0;
(3)设置文件描述符为非阻塞
为什么要设置非阻塞,可以考虑要是文件描述符是阻塞的,那么当send/recv时要是缓冲区没有数据,就停在那了不动了,最后的情况就是所有的数据读写都要严格按照程序设定的时序走,这已经不是聊天室了。
int setnonblocking(int fd)
{
int old_option = fcntl(fd , F_GETFL);
int new_option = fcntl(fd, old_option | O_NONBLOCK);
fcntl(fd, F_SETFL, new_option);
return old_option;
}
(4)客户端的名字获取和客户端发送/接收 缓冲区的拷贝
一开始,就收到客户端以 ‘*’ 开头的字符串,我们认为后面跟的是名字,将他截取下来,保存到成员变量中
//发来的是名字
//--可以增加改名字的功能
if ( (ret > 0) && users[connfd].buf[0] == '*') {
strncpy(users[connfd].my_name, users[connfd].buf + 1, (strlen(users[connfd].buf)-2) ); //这里-2因为要去掉*和最后的n
printf("*************** ++ *************** new client : %sn", users[connfd].my_name);
continue;
}
然后再发来的字符串我们都认为是聊天的内容,接收后先保存在 char buf[BUFFER_SIZE]; 中,当确认接收完毕后,做一个for循环,将除了发送的客户端以外的其他客户端的 char *write_buf; 设为聊天内容,同时这些客户端的相应pollfd结构体开始监听POLLIN事件
(5)整个实现流程就是poll循环监听文件描述符集,当有事件发生时,for循环找到发生事件的那个文件描述符,然后判断是什么事件,进行相应处理,若是POLLIN事件的话,将数据保存在成员变量buf中后,在利用一个for循环将数据写到除了该文件描述符以外的其他文件描述符对应的user结构体的write_buf中,并修改事件监听为POLLOUT。继续循环
4、源码.c
服务器端
#include "chat_room_server.h"
int setnonblocking(int fd)
{
int old_option = fcntl(fd , F_GETFL);
int new_option = fcntl(fd, old_option | O_NONBLOCK);
fcntl(fd, F_SETFL, new_option);
return old_option;
}
int main(void)
{
int lfd;
int ret;
struct sockaddr_in addr_server;
bzero(&addr_server, sizeof(addr_server));
addr_server.sin_port = htons(MY_PORT);
addr_server.sin_family = AF_INET;
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = socket(AF_INET, SOCK_STREAM, 0);
int on = 1;
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
printf("setsockopt () errn");
return -1;
}
if (lfd < 0) {
printf("socket() errn");
return -1;
}
ret = bind(lfd, (struct sockaddr*)&addr_server, sizeof(addr_server));
if (ret < 0) {
printf("bind() errn");
return -1;
}
ret = listen(lfd, 10);
if (ret < 0) {
printf("listen() errn");
return -1;
}
/*分配客户端数据*/
//char temp_buf[BUFFER_SIZE];
client_data* users = (client_data *)malloc(sizeof(client_data) * 10);
pollfd fds[MAX_USER_NO + 1];
int user_count = 0;//记录用户连接数
for (int i = 0; i <= MAX_USER_NO; i++) {
fds[i].fd = -1;
fds[i].events = 0;
}
/*监听文件描述符*/
fds[0].fd = lfd;
fds[0].events = POLLIN | POLLERR;
fds[0].revents = 0;
while (1)
{
ret = poll(fds, user_count + 1, -1);//阻塞等待
if (ret < 0) {
printf("poll() errn");
return -1;
}
for (int i = 0; i < user_count + 1; i++) {
/*意味着有新的客户端请求连接*/
if ((fds[i].fd == lfd) && (fds[i].revents & POLLIN)) {
struct sockaddr_in addr_client;
socklen_t len_clientaddr = sizeof(addr_client);
int cfd = accept(lfd, (struct sockaddr*)&addr_client, &len_clientaddr);
if (cfd < 0) {
printf("accept() errn");
continue;
}
//printf("a new client is connectn");
if (user_count >= MAX_USER_NO) {
const char* info = "too many client! n";
printf("%sn",info);
send(cfd, info , strlen(info),0);
close(cfd);
continue;
}
/*连接新的客户端,分配客户端信息*/
user_count++;
setnonblocking(cfd);
/*采取文件描述符作为下标*/
users[cfd].address = addr_client;
fds[user_count].fd = cfd;
fds[user_count].events = POLLIN | POLLRDHUP | POLLERR;
fds[user_count].revents = 0;
printf("a new user is coming ,now have %d users in roomn", user_count);
}
else if (fds[i].revents & POLLERR) {
printf("get POLLERR from fd:%dn", fds[i].fd);
char err[100];
memset(err, '