博客 约 20 分钟

实现Linux 聊天室(高性能服务器设计基础)

原载 来源

Linux 聊天室(高性能服务器设计基础)

实现这次聊天室可以很好的帮助理解Linux的网络编程。 深入了解TCP、UDP的运行流程。 了解如何使用socket、connect、bind、send、recv等相关函数以及它们的作用。 我会先从简单的实现开始。然后逐步进行优化。慢慢体现为什么要使用这些函数。

架构

环境:Linux

聊天室采用的是C/S的TCP架构

  • TCP流程图
graph LR

A[服务器端]-->B(Socket)-->C(bind)-->D(listen)-->E(accept)-->F(recv/recvfrom)
F-->G(send/sendto)
G-->H(socket)

A2[客户端]-->B2(Socket)---->E2(connect)

E2(connect)-->|请求成功|G2(send/sendto)-->F2(recv/recvfrom)-->H2(socket)
  • UDP流程图
graph LR

A[服务器端]-->B(Socket)-->C(bind)-->F(recv/recvfrom)
F-->G(send/sendto)-->H(close)

A2[客户端]-->B2(Socket)--->G2(send/sendto)-->F2(recv/recvfrom)-->H2(close)

头文件

#ifndef __TCP_NET_SOCKET_H__
#define __TCP_NET_SOCKET_H__

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>

#define COMMON_ERROR  -1

#define PERROR(err, ret) \
    do { \
	    perror(err); \
        if (ret) { \
            exit(EXIT_FAILURE); \
        } \
    } while (0)


#define PERROR_C(err, ret, sfd) \
    do { \
        perror(err); \
        if (ret) { \
            close(sfd); \
            exit(EXIT_FAILURE); \
        } \
    } while (0)

#define PERROR_CC(err, ret, sfd, cfd) \
    do { \
        perror(err); \
        if (ret) { \
            close(sfd); \
            close(cfd); \
            exit(EXIT_FAILURE); \
        } \
    } while (0)

/*初始化操作:Socket创建,bind定位,listen建立*/
extern int tcp_init(const char* ip, int port);

/*用于服务端的接受,接受请求成功返回新的Socket new_fd*/
extern int tcp_accept(int sfd);

/*用于客户端的连接*/
extern int tcp_connect(const char* ip, int port);

#endif //__TCP_NET_SOCKET_H__

通用函数封装

这里封装了3个函数。 tcpinit:服务端调用,为客户端的连接提供接口。 tcpconnect:客户端调用,向服务端发起连接。 tcp_accept:服务端调用,接受客户端的连接。
#include "tcp_net_socket.h"

#define PORT 8888
/*初始化操作:Socket创建,bind定位,listen建立*/
int tcp_init(const char* ip, int port)
{
	int ret = 0;
	int sfd;
	//设置地址族IPV4、IPV6和端口号。0则自动分配。
	
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(struct sockaddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(port);
	serveraddr.sin_addr.s_addr = inet_addr(ip);
	//创建一个Socket
	
	sfd = socket(AF_INET, SOCK_STREAM, 0);
	PERROR("socket", (COMMON_ERROR == sfd));
	//绑定该Socket
	
	ret = bind(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
	PERROR_C("bind", (COMMON_ERROR == ret), sfd);
	//监听该Socket

	ret = listen(sfd, 10);
	PERROR_C("listen", (COMMON_ERROR == ret), sfd);

	return sfd;

}
/*用于服务端的接受,接受请求成功返回新的Socket new_fd*/
int tcp_accept(int sfd)
{
	int addrlen;
	int new_fd;
	struct  sockaddr_in clientaddr;

	memset(&clientaddr, 0, sizeof(struct sockaddr));
	addrlen = sizeof(struct sockaddr);
	//接受连接成功则创建新的Socket为new_fd

	new_fd = accept(sfd, (struct sockaddr*)&clientaddr, &addrlen);
	PERROR_C("accept", (COMMON_ERROR == new_fd), sfd);

	printf("%s%d success connect! \n", inet_ntoa(clientaddr.sin_addr));

	return new_fd;
}
/*用于客户端的连接*/
int tcp_connect(const char* ip, int port)
{
	int sfd;
	int ret = 0;
	struct sockaddr_in serveraddr;
	sfd = socket(AF_INET, SOCK_STREAM, 0);
	PERROR("socket", (COMMON_ERROR == sfd));

	memset(&serveraddr, 0, sizeof(struct sockaddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(port);
	serveraddr.sin_addr.s_addr = inet_addr(ip);

	ret = connect(sfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
	PERROR_C("connect", (COMMON_ERROR == ret), sfd);

	return sfd;
}

服务端版本1.0

#include "tcp_net_socket.h"

void server_start(int argc, char* argv[])
{
	int sfd;
	int ret = 0;

	if (argc < 3)
	{
		printf("param error");
	}

	//tcp初始化

	sfd = tcp_init(argv[1], atoi(argv[2]));

	while (1)
	{
		//接受连接

		int cfd = tcp_accept(sfd);
		char buf[512] = { 0 };
		char msg[32] = { "Hello I am server." };
		//接受客户端传来的数据并存入buf中。

		ret = recv(cfd, buf, sizeof(buf), 0);
		PERROR_CC("recv", (COMMON_ERROR == ret), sfd, cfd);
		puts(buf);
		//从buf中取出,并向cfd客户端发送数据

		ret = send(cfd, msg, strlen(msg), 0);
		PERROR_CC("send", (COMMON_ERROR == ret), sfd, cfd);
		close(cfd);
	}
	close(sfd);
}

int main(int argc, char* argv[])
{
	server_start(argc, argv);
	return 0;
}

客户端版本1.0

#include "tcp_net_socket.h"

void client_start(int argc, char* argv[])
{
	int sfd;
	int ret = 0;
	char buf[512] = { 0 };
	char msg[32] = { "Hello I am client." };

	if (argc < 3)
	{
		printf("param error");
	}
	//发送连接请求

	sfd = tcp_connect(argv[1], atoi(argv[2]));

	send(sfd, msg, strlen(msg), 0);
	recv(sfd, buf, sizeof(buf), 0);
	puts(buf);
	close(sfd);
}

int main(int argc, char* argv[])
{
	client_start(argc, argv);
	return 0;
}

1.0版本

搭建了客户端和服务端的基本框架 实现了客户端和服务端的连接

分别编译客户端和服务端并运行

gcc -o tcp_net_client tcp_net_client.c tcp_net_socket.c

gcc -o tcp_net_server tcp_net_server.c tcp_net_socket.c

在这里插入图片描述 版本1.0只实现了基本的连接,客户端发送消息并接受服务端返回的消息后生命周期就结束了。如果我们要实现一个聊天室,首先要实现客户端可以发送消息并且与服务器进行交互。

客户端版本2.0

#include "tcp_net_socket.h"

void client_start(int argc, char* argv[])
{
	int sfd;
	int ret = 0;
	char buf[512] = { 0 };
	char msg[32] = { "Hello I am client." };

	if (argc < 3)
	{
		printf("param error");
	}
	//发送连接请求

	sfd = tcp_connect(argv[1], atoi(argv[2]));
	while(1)
	{
		scanf("%s", msg);
		send(sfd, msg, strlen(msg), 0);
		recv(sfd, buf, sizeof(buf), 0);
		puts(buf);
	}
	close(sfd);
}

int main(int argc, char* argv[])
{
	client_start(argc, argv);
	return 0;
}

1.2版本

客户端增加while循环,在scanf输入后发送消息到服务端 服务端将收到的消息返回给客户端,实现了一个基本的循环通信 要实现聊天室,服务端要同时接受多个客户端的连接,并且转发其他客户端的消息,我们可以使用select实现TCP循环服务器。 记录每个客户端的描述符并进行监听。

服务端版本2.0

#include "tcp_net_socket.h"

void server_start(int argc, char* argv[])
{
	int sfd;
	int ret = 0;
	int fd[1000] = { 0 };
	int MaxFd, i = 0, j;
	fd_set ReadFd, tmpfd;
	char buf[64] = { 0 };
	struct timeval timeout = { 5, 0 };
	if (argc < 3)
	{
		printf("param error");
	}

	//tcp初始化

	sfd = tcp_init(argv[1], atoi(argv[2]));

	FD_ZERO(&ReadFd);
	FD_SET(sfd, &ReadFd);
	MaxFd = sfd;


	while (1)
	{
		tmpfd = ReadFd;
		ret = select(MaxFd + 1, &tmpfd, NULL, NULL, NULL);
		PERROR("select", (COMMON_ERROR == ret));
		//有连接请求

		if (FD_ISSET(sfd, &tmpfd))
		{
			//选择可用的fd标记位

			for (j = 0; j < i; j++)
			{
				if (0 == fd[j])
				{
					break;
				}
			}
			//接受连接

			fd[j] = tcp_accept(sfd);
			PERROR("tcp_accept", (COMMON_ERROR == ret));
			//更新最大描述符,并新的文件描述符添加到集合
			
			if (MaxFd < fd[j])
			{
				MaxFd = fd[j];
			}
			FD_SET(fd[j], &ReadFd);
			//setsockopt
			ret = setsockopt(fd[j], SOL_SOCKET, SO_RCVTIMEO, (char*) & timeout, sizeof(struct timeval));
			PERROR("setsockopt", (COMMON_ERROR == ret));
			if (j == i)
			{
				i++;
			}
		}
		else
		{
			for (j = 0; j < i; j++)
			{
				if (FD_ISSET(fd[j], &tmpfd))
				{
					char msg[128] = "User ";

					int tmp = j;
					ret = recv(fd[j], buf, sizeof(buf), 0);
					PERROR("recv", (COMMON_ERROR == ret));
					//处理客户端意外中断

					if (0 == ret)
					{
						FD_CLR(fd[j], &tmpfd);
						i--;
					}
					//向其他客户端转发信息
					msg[5] = j + '0';
					msg[6] = ':';
					strcat(msg, buf);
					puts(msg);
					for (j = 0; j < i; j++)
					{
						if (tmp != j)
						{
							ret = send(fd[j], msg, strlen(msg), 0);
							PERROR("send", (COMMON_ERROR == ret));
						}
					}
					memset(buf, 0, sizeof(buf));
					memset(buf, 0, sizeof(msg));
					break;
				}
			}

		}
	}

	close(sfd);
}



int main(int argc, char* argv[])
{
	server_start(argc, argv);
	return 0;
}

版本2.0

服务端监听自己的描述符和客户端的描述符分别进行处理连接请求和消息转发。 可以同时接受多个客户端的连接,并转发其他客户端的消息。 此时客户端的写法存在一个问题,读写同步了。scanf会阻塞recv消息的接受。我们需要改成异步的。 增加一个线程用于接受服务端的信息。 增加描述符监听的写接口是否可用,可用才会进行消息发送。

客户端版本3.0

#include "tcp_net_socket.h"

void* thread_recv_cb(void *data)
{
	int *tmpfd = (int*)data;
	int sfd = *tmpfd;
	char buf[512] = { 0 };
	fd_set ReadSet;
	FD_ZERO(&ReadSet);
	FD_SET(sfd, &ReadSet);
	while (1)
	{
		if (FD_ISSET(sfd, &ReadSet))
		{
			recv(sfd, buf, sizeof(buf), 0);
			puts(buf);
			memset(buf, 0x00, sizeof(buf));
		}
	}
}

void client_start(int argc, char* argv[])
{
	int sfd;
	fd_set WriteSet;
	int ret = 0;

	char msg[32] = { "Hello I am client." };
	//int optval = 1;
	int optval_get;
	int optlen = sizeof(optval_get);
	pthread_t id1;
	int i = 0;
	if (argc < 3)
	{
		printf("param error");
	}
	//发送连接请求
	sfd = tcp_connect(argv[1], atoi(argv[2]));
	ret = pthread_create(&id1, NULL, thread_recv_cb, (void*) & sfd);
	//setsockopt
	//ret = setsockopt(sfd, SOL_SOCKET, SO_PRIORITY, &optval, sizeof(optval));
	//if (0 != ret)
	//{
	//	printf("setsockopt failed! ret:%s\n", strerror(errno));
	//}

	//getsockopt
	ret = getsockopt(sfd, SOL_SOCKET, SO_TYPE, &optval_get, &optlen);
	if (0 != ret)
	{
		printf("getsockopt failed! ret:%s\n", strerror(errno));
	}
	else
	{
		switch (optval_get)
		{
		case 1:
			printf("IS TCP CONNECT\n");
			break;
		case 2:
			printf("IS UDP CONNECT\n");
			break;
		}
	}
	FD_ZERO(&WriteSet);
	FD_SET(sfd, &WriteSet);
	while (1)
	{
		ret = select(sfd + 1, NULL, &WriteSet,  NULL, NULL);
		if (-1 == ret)
		{
			perror("select");
		}

		if (FD_ISSET(sfd, &WriteSet))
		{
			if (i > 0)
			{
				scanf("%s", msg);
			}
			if ("exit" == msg)
			{
				break;
			}
			ret = send(sfd, msg, strlen(msg), 0);
			if (ret > 0)
			{
				//printf("send success\n");
				i++;
			}
			else
			{
				printf("send failed:%d\n", ret);
			}
		}
	}
	close(sfd);

}

int main(int argc, char* argv[])
{
	client_start(argc, argv);
	return 0;
}

结果

在这里插入图片描述

  • 客户端的消息接收与发送更改为异步。
  • 服务端可以接收到所有客户端发送的消息。
  • 聊天室的基本功能已经实现。

问题

此时的聊天室没有过多的错误处理。如果客户端突然中断或者服务端突然中断。会产生异常场景。 我们可以用timer定时器加入心跳机制,在服务端和客户端之间建立心跳,如果心跳中断可以尝试重新连接,失败后主动断开。

原文发布于 CSDN