主要还是为了熟练熟练网络编程的东西,这些天有时间将网络编程总细节一点的地方又看了看,主要是一些网络编程中的库函数:setsockopt、TCP、UDP这些东西。
之前刚刚看网络编程方面知识的时候在windows上写了个很简单的网络传输的小程序(就是这个),那个时候就想着能不能写一个聊天室类的程序。刚好这些天看书时候书上有一个聊天室的源代码程序,正好这些天也比较闲,将源码看了一遍后感觉实现起来逻辑还是比较清晰的,于是就动手吧,多敲敲找找感觉也没错。
运行服务器端,等待客户端连接。
当客户成功连接服务器后可以向服务器发送数据,服务器接收到数据后将接收到的数据发送给其他连接上的客户端。
客户端连接后发送*+name,可以设置自己在其他客户端上输出时的名字
(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 : %s\n", 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。继续循环
服务器端
#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 () err\n");
return -1;
}
if (lfd < 0) {
printf("socket() err\n");
return -1;
}
ret = bind(lfd, (struct sockaddr*)&addr_server, sizeof(addr_server));
if (ret < 0) {
printf("bind() err\n");
return -1;
}
ret = listen(lfd, 10);
if (ret < 0) {
printf("listen() err\n");
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() err\n");
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() err\n");
continue;
}
//printf("a new client is connect\n");
if (user_count >= MAX_USER_NO) {
const char* info = "too many client! \n";
printf("%s\n",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 room\n", user_count);
}
else if (fds[i].revents & POLLERR) {
printf("get POLLERR from fd:%d\n", fds[i].fd);
char err[100];
memset(err, '\0', 100);
socklen_t len_err = sizeof(err);
if ((getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, &err, &len_err)) < 0) {
printf("getsockopt err\n");
}
printf("err: %s", err);
continue;
}
else if (fds[i].revents & POLLRDHUP) {
users[fds[i].fd] = users[fds[user_count].fd];
close(fds[i].fd);
printf("fds[%d].revents & POLLRDHUP so a client leave\n", i);
fds[i] = fds[user_count];
user_count--;
i--;
continue;
}
else if (fds[i].revents & POLLIN) {
int connfd = fds[i].fd;
memset(users[connfd].buf, '\0', BUFFER_SIZE);
//memset(users[connfd].my_name, '\0', MAX_NAME_LEN);
//memset(users[connfd].write_buf, '\0', BUFFER_SIZE);
ret = recv(connfd, users[connfd].buf, BUFFER_SIZE - 1, 0);
//发来的是名字
//--可以增加改名字的部分
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 : %s\n", users[connfd].my_name);
//printf("test users[connfd].my_name: %s\n", users[connfd].my_name);
continue;
}
//printf("test users[connfd].my_name outside: %s\n", users[connfd].my_name);
printf("recv the msg from:%s msg:%s\n", users[connfd].my_name , users[connfd].buf);
if (ret < 0) {
/*因为是非阻塞,这里进行多一次判断*/
if (ret != EAGAIN) {
printf("fd: %d recv err and will close this fd \n", connfd);
close(connfd);
/*下面两步是将该处的用户数据和文件描述符改成最后一个,i--重新进入文件描述符的寻找中去,
就是用最后一个文件描述符代替这个位置的文件描述符,然后对这个位置在过一遍流程*/
users[fds[i].fd] = users[fds[user_count].fd];
fds[i] = fds[user_count];
i--;
user_count--;
}
}
else if (ret == 0) {
}
/*正常收到数据,准备将这些数据发送给其他的客户端*/
else
{
for (int j = 1; j <= user_count; j++) {
if (fds[j].fd == connfd) { //为发送数据的客户端
continue;
}
/*修改事件为POLLOUT*/
fds[j].events |= ~POLLIN;
fds[j].events |= POLLOUT;
users[fds[j].fd].write_buf = (char *)malloc(BUFF!:ER_SIZE);
//strncpy(temp_buf, users[connfd].buf, BUFFER_SIZE);
//sprintf(temp_buf, "%s", users[connfd].buf);
sprintf(users[fds[j].fd].write_buf, "%s :%s", users[connfd].my_name, users[connfd].buf);
//printf("No:%d \t test when sprintf users[connfd].write_buf:%s\n", fds[j].fd, users[fds[j].fd].write_buf);
//users[fds[j].fd].write_buf = users[connfd].buf;
}
}
}
/*这里识
别事件POLLOUT 向其他客户端发送某个客户端的数据*/
else if (fds[i].revents & POLLOUT) {
int connfd = fds[i].fd;
if (!users[connfd].write_buf) {
continue;
}
//printf("No:%d \t test befor send users[connfd].write_buf:%s\n", connfd,users[connfd].write_buf);
if ( (send(connfd, users[connfd].write_buf, strlen(users[connfd].write_buf),0)) < 0 ) {
printf("send err at fd:%d", connfd);
}
free(users[connfd].write_buf);
users[connfd].write_buf = NULL;
//memset(users[connfd].write_buf, '\0', sizeof(BUFFER_SIZE));
fds[i].events |= ~POLLOUT;
fds[i].events |= POLLIN;
}
}
}
free(users);
close(lfd);
return 0;
}
客户端
#include "chat_room_client.h"
int main()
{
int sfd;
struct sockaddr_in addr_server;
bzero(&addr_server, sizeof(addr_server));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(MY_PORT);
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
return -1;
if (connect(sfd, (struct sockaddr*)&addr_server, sizeof(addr_server)) < 0) {
close(sfd);
return -1;
}
printf("connect is success\n");
/*注册文件描述符,0用于输出,1用于读socket上的内容*/
pollfd fds[2];
fds[0].fd = 0;
fds[0].events = POLLIN | POLLOUT;
fds[0].revents = 0;
fds[1].fd = sfd;
fds[1].events = POLLIN | POLLRDHUP; //服务器关闭连接时,设置为POLLRDHUP
fds[1].revents = 0;
char read_buf[BUFFER_SIZE]; //64
/*准备两个管道*/
int pipefd[2];
int ret = pipe(pipefd);
if (ret < 0)
return -1;
while (1)
{
ret = poll(fds, 2, -1);
if (ret < 0) {
printf("poll() error\n");
return -1;
}
/*服务器关闭*/
if (fds[1].revents & POLLRDHUP) {
printf("server close the connect!\n");
break;
}
/*socket缓冲区有数据可读*/
if (fds[1].revents & POLLIN) {
memset(read_buf, '\0', BUFFER_SIZE);
recv(fds[1].fd, read_buf, BUFFER_SIZE - 1, 0);
printf("%s\n", read_buf);
}
/*客户端用户输入数据,使用splice函数,实现零拷贝*/
/*ssize_t splice(int fd_in,loff_t* off_t,int fd_out,loff_t* off_out,size_t len,unsigned int flags);
fd_in:待输入数据的文件描述符.
off_t:如果fd_in是一个管道文件描述符,那么off_t参数必须是NULL,表示从数据流的当前偏移位置读入;如果fd_in不是一个管道文件描述符(例如socket),则它将指出具体的偏移位置.
len:指定移动数据的长度.
flags:则控制数据如何移动,它可以被设置为下表中值的按位异或.*/
if (fds[0].revents & POLLIN) {
ret = splice(0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE); //0是标准输入的文件描述符
ret = splice(pipefd[0], NULL, sfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
//为什么不直接将0->sfd? 因为使用splice函数时,fd_in和fd_out必须至少有一个管道文件描述符
}
}
close(sfd);
return 0;
}
服务器
#pragma once
#define _GNU_SOURCE 1
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_SIZE 1024
#define MAX_USER_NO 10
#define MAX_NAME_LEN 10
#define MY_PORT 10086
#define FD_LIMIT 65535
struct client_data
{
sockaddr_in address;
char my_name[MAX_NAME_LEN];
char *write_buf;
char buf[BUFFER_SIZE];
};
//设置文件描述符为非阻塞
int setnonblocking(int fd);
客户端
#pragma once
#define _GNU_SOURCE 1
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUFFER_SIZE 64
#define MY_PORT 10086
添加了makefile
进入该目录,命令行输入
make chat_room_client 编译客户端
make chat_room_server 编译服务器端
make all 编译服务器和客户端
make clean 清除