linux简易聊天室的实现

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 : %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。继续循环

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 () 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;
}
5、源码.h

服务器

#pragma once
#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <fcntl.h>
#include <errno.h>

#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 <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <fcntl.h>
#include <errno.h>

#define BUFFER_SIZE 64
#define MY_PORT 10086
6、这里是我在虚拟机运行测试的程序打包

这里是资源

添加了makefile
进入该目录,命令行输入
make chat_room_client 编译客户端
make chat_room_server 编译服务器端
make all 编译服务器和客户端
make clean 清除

发布了46 篇原创文章 · 获赞 13 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_42718004/article/details/90718606
今日推荐