BBS客户端服务器的编写

news2025/1/11 22:50:09

根据网络编程中的内容,我们本篇文章将讲解一个bbs通信的项目,首先让我们了解一下什么是bbs.

一、bbs介绍

BBS,即Bulletin Board System的缩写,中文译为“电子公告板系统”或“网络论坛”。它是一个在网络上进行信息交流和讨论的平台。早期的BBS主要用于公布股市价格等信息,只能在苹果计算机上运行。随着个人计算机的普及,BBS逐渐转移到个人计算机上,功能也得到了扩展,用户可以在BBS上发布文章、收发电子邮件、交流聊天、发布广告等。BBS的发展历程可以追溯到20世纪70年代,最早的BBS系统出现在美国芝加哥。在中国,第一个BBS站出现在1991年。如今,BBS仍然是一种常见的网络交流方式,被广泛应用于教学、推广、地方交流和一般性讨论等领域。

二、bbs客户端

2.1、客户端流程图

简单来看

  1. 使用socket创建通讯句柄
  2. 使用connect连接到主机
  3. 使用select进行键盘和网络的多路选择
    • 如果有键盘数据,则读入键盘数据并发送到网络
    • 如果有网络数据,则接收网络数据并上屏显示
  4. 判断是否接收到退出符,如果是,则使用close断开网络连接并结束程序

 2.2实现代码

首先进行初始化,使用套接字与服务器进行连接

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

int init_socket(char *ip)
{
	//1. 创建一个套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	//2 定义一个 sockaddr_in 结构体,并初始化

	struct  sockaddr_in  serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(2000); //  h:host  n:network s : short
	inet_aton(ip, &serveraddr.sin_addr);

	//3. connect 服务器
	connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	
	
	return  sockfd;
}

init_socket函数的作用是初始化一个套接字,并使用该套接字连接到指定的服务器。

函数接受一个参数ip,表示服务器的IP地址。函数内部首先使用socket系统调用创建了一个套接字,并指定了地址族(AF_INET表示IPv4)和套接字类型(SOCK_STREAM表示面向连接的TCP协议)。然后,函数定义了一个sockaddr_in结构体变量serveraddr,用于存储服务器的地址信息。

接下来,函数对serveraddr结构体进行了初始化。其中,sin_family字段被设置为AF_INET,表示使用IPv4地址族。sin_port字段被设置为htons(2000),表示服务器的端口号是2000。htons函数用于将主机字节序的16位整数转换为网络字节序。sin_addr字段被设置为使用inet_aton函数将传入的IP地址字符串转换为网络字节序的二进制形式。

最后,函数使用connect系统调用尝试连接到服务器。如果连接成功,则返回套接字描述符sockfd;如果连接失败,则返回错误代码。

 

 主函数

int main(int argc, char *argv[])
{
	char buf[1024]={0};
	int result,fd;
	fd_set rdset;
	
	int sockfd = init_socket(argv[1]);
	while(1)
	{
		FD_ZERO(&rdset);
		FD_SET(sockfd, &rdset);
		FD_SET(0, &rdset);
		if(select(sockfd+1, &rdset, NULL, NULL, NULL)<0)
		{
			perror("select:");
			return -1;
		}
		if(FD_ISSET(sockfd,&rdset))//网络有数据可读
		{
			memset(buf,0,sizeof(buf));
			result = recv(sockfd, buf, sizeof(buf)-1, 0);
			if(result < 0)
			{
				perror("recv:");
				return -1;
			}
			else if(result == 0)
			{
				printf("服务器断开连接\n");
				break;
			}
			else{
				buf[result] = 0; //放 \0 作为buf的结束标志
				printf("%s\n",buf);
				
			}
		}
		if(FD_ISSET(0, &rdset)) //键盘有数据可读
		{
			memset(buf,0,sizeof(buf));
			if(fgets(buf, sizeof(buf), stdin))
			{

				write(sockfd,buf,strlen(buf));
				
			}
		}
	}
}

这段代码是C语言编写的一个网络通信程序的主函数。它实现了一个简单的客户端,可以与服务器进行数据的发送和接收。

函数的开头定义了一些变量:

  • char buf[1024]:用于存储接收到的数据或要发送的数据的缓冲区。
  • int result:用于存储接收或发送操作的结果。
  • fd_set rdset:用于存储要监视的文件描述符的集合。

然后,程序调用init_socket函数来初始化套接字,并将服务器的IP地址作为参数传递给它。init_socket函数返回一个套接字描述符sockfd,用于与服务器进行通信。

接下来是一个无限循环,用于不断监视套接字和标准输入是否有数据可读。循环的开始,程序使用FD_ZERO函数清空文件描述符集合rdset,然后使用FD_SET函数将套接字描述符sockfd和标准输入的文件描述符0添加到集合中。

然后,程序调用select函数来监视文件描述符集合。如果select函数返回值小于0,表示发生了错误,程序会调用perror函数打印错误信息并返回-1。

如果select函数返回值大于0,表示有文件描述符可读。程序首先检查套接字描述符是否可读,即FD_ISSET(sockfd, &rdset)是否为真。如果是真,表示网络上有数据可读,程序会使用recv函数接收数据,并将接收到的数据存储在缓冲区buf中。如果接收操作成功,程序会将接收到的数据打印到屏幕上。如果接收到的数据长度为0,表示服务器断开了连接,程序会打印提示信息并退出循环。

如果套接字描述符不可读,程序会检查标准输入是否可读,即FD_ISSET(0, &rdset)是否为真。如果是真,表示键盘上有数据可读,程序会使用fgets函数读取一行数据,并将读取到的数据存储在缓冲区buf中。然后,程序会使用write函数将数据发送给服务器。

总的来说,这个主函数实现了一个简单的网络通信客户端,可以与服务器进行数据的发送和接收。它使用select函数来监视套接字和标准输入,并根据不同的情况进行相应的操作。

三、bbs服务器

3.1 服务器流程

  1. 使用socket创建监听句柄
  2. 使用bind绑定端口
  3. 使用listen监听端口
  4. 使用accept等待客户接入,并使用pthread_create创建线程与客户交互
  5. 在线程中,使用write发送选择菜单,并使用read等待客户选择
    • 如果选择有效,则调用addClient添加新客户到链表中
    • 使用read等待客户发布消息,并将消息保存到相应类型的文件中
    • 调用multicastMsg将消息转发到相同类型的其他客户
  6. 判断是否接收到退出标识,如果是,则调用removeClient删除该客户并关闭通讯句柄,然后线程退出

3.2 实现代码

首先进行一定义



//定义类型枚举量
typedef enum ClientType
{
	NEWS_TYPE,
	ENTERTAINMENT_TYPE,
	SUPPLY_TYPE
}ClientType;

//定义客户节点量
typedef struct ClientNode
{
	int sockfd;
	enum ClientType type;
	char logname[20]; //登录名
	struct ClientNode * next;
}ClientNode;

ClientNode *head=NULL;
pthread_rwlock_t lockList = PTHREAD_RWLOCK_INITIALIZER;
pthread_rwlock_t lockFile = PTHREAD_RWLOCK_INITIALIZER;

char *name_msg = "请输入登录名:\n";
char *menu="\n"
" -------------------------\n"	
"      登录类型选择              \n"
"      1)  新闻              \n"
"      2)  娱乐              \n"
"      3)  交易              \n"
" -------------------------\n"	
"请选择登录的类目:";
  • ClientType:这是一个枚举类型,表示了客户端的类型,包括新闻(NEWS_TYPE)、娱乐(ENTERTAINMENT_TYPE)和供应(SUPPLY_TYPE)三种类型。
  • ClientNode:这是一个结构体类型,表示了链表中的一个节点,包含了客户端的套接字描述符(sockfd)、类型(type)、登录名(logname)以及指向下一个节点的指针(next)。
  • head:这是一个指向ClientNode类型的指针,表示了链表的头节点。初始值为NULL,表示链表为空。
  • lockList和lockFile:这两个是读写锁(pthread_rwlock_t)类型的变量,用于对链表和文件进行并发访问的控制。初始化为PTHREAD_RWLOCK_INITIALIZER,表示锁已经被初始化。

 初始化套接字,等待客户端请求

int init_socket(char *ip)
{
	//1. 创建一个套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	//2 定义一个 sockaddr_in 结构体,并初始化

	struct  sockaddr_in  serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(2000); //  h:host  n:network s : short
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	// INADDR_ANY  有内核帮你找一个合适的网卡IP地址
	socklen_t addrlen = sizeof(serveraddr);
	int on=1;
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
	
	int r =  bind(sockfd,(struct sockaddr *)&serveraddr,addrlen);
	if(r < 0)
	{
		perror("bind:");
		return -1;
	}
	r = listen(sockfd, 5);
	if(r < 0)
	{
		perror("listen:");
		return -1;
	}
	
	return  sockfd;
}

使用socket系统调用创建一个套接字,指定地址族为AF_INET(表示IPv4),类型为SOCK_STREAM(表示面向连接的TCP协议)。

定义一个sockaddr_in结构体变量serveraddr,用于存储服务器的地址信息。

初始化serveraddr结构体的各个字段:

sin_family字段被设置为AF_INET,表示使用IPv4地址族。

sin_port字段被设置为htons(2000),表示服务器的端口号为2000。htons函数用于将主机字节序的16位整数转换为网络字节序。

sin_addr.s_addr字段被设置为htonl(INADDR_ANY),表示服务器可以接受来自任何IP地址的连接请求。htonl函数用于将主机字节序的32位整数转换为网络字节序。

使用setsockopt系统调用设置套接字选项,允许多个进程或线程同时绑定到同一个端口号上。

使用bind系统调用将套接字绑定到指定的IP地址和端口号上。如果绑定失败,则打印错误信息并返回-1。

使用listen系统调用开始监听套接字,等待客户端的连接请求。如果监听失败,则打印错误信息并返回-1。

如果一切正常,则返回套接字描述符sockfd。

 使用头插法,插入客户,形成一个链表

void insertNode(ClientNode * p)
{
	p->next = head;

	head = p;
}

 客户端断开连接后,删除此节点

void DeleteNode(ClientNode *client)
{
	ClientNode *p = head;
	ClientNode *q = head;
	while(p)
	{
		if(p->sockfd == client->sockfd)
		{
			if(p == q)
			{
				//你要删除的是 第一个节点head
				head = p->next;
				
			}
			q->next = p->next;
			free(p);
			break;
		}
		q = p; //保存上一节点
		p = p->next; 
	}
}

  1. 定义两个指针变量p和q,其中p用于遍历链表,q用于保存p的前一个节点。
  2. 使用while循环遍历链表,直到找到要删除的节点或到达链表的末尾。
  3. 在循环内部,使用if语句判断当前节点是否为要删除的节点。如果当前节点的sockfd字段与要删除节点的sockfd字段相等,则表示找到了要删除的节点。
  4. 如果要删除的节点是链表的第一个节点,即p == q,则直接将头节点指向下一个节点,即head = p->next;。
  5. 如果要删除的节点不是链表的第一个节点,则将q的next指针指向p的下一个节点,即q->next = p->next;,从而将p从链表中删除。
  6. 使用free函数释放被删除节点所占用的内存空间。
  7. 使用break语句退出循环。

 发送历史记录

void sendHistory(char *filename,int newfd)
{
	char buf[1024]={};
	int n;
	FILE *fp = fopen(filename, "r");
	if(fp==NULL)
	{
		perror("fopen:");
		return ;
	}
	while(fgets(buf, sizeof(buf),fp))
	{
		write(newfd, buf, strlen(buf));
	}
	fclose(fp);
}
  1. 定义一个字符数组buf作为缓冲区,用于存储从文件中读取的数据。
  2. 使用fopen函数以只读模式打开指定文件。如果文件打开失败,则打印错误信息并返回。
  3. 使用fgets函数从文件中读取一行数据,并将其存储在缓冲区buf中。如果读取成功,则使用write函数将数据发送给套接字连接。
  4. 重复步骤3,直到文件中的所有数据都读取完毕。
  5. 使用fclose函数关闭文件。

保存文件

void save2File(char *filename, char *buf)
{
    FILE *fp;
    fp = fopen(filename, "a+"); // 以追加和读写模式打开文件
    
    if (fp != NULL) {
        fputs(buf, fp); // 将数据写入文件末尾
        fclose(fp); // 关闭文件
    } else {
        perror("Error opening file"); // 如果文件打开失败,打印错误信息
    }
}

函数的实现步骤如下:

  1. 使用fopen函数以追加和读写模式("a+")打开指定的文件。如果文件不存在,则会创建一个新文件。如果文件已经存在,则会在文件末尾追加数据。
  2. 如果文件打开成功(fp不为NULL),则使用fputs函数将数据缓冲区buf中的内容写入到文件中。
  3. 无论文件打开是否成功,都需要使用fclose函数关闭文件。
  4. 如果文件打开失败(fp为NULL),则使用perror函数打印错误信息。

将一条消息从一个客户端节点广播到所有类型相同的其他客户端节点

void multicastNode(ClientNode * client,char * buf)
{
	ClientNode *p = head;
	while(p)
	{
		//类型和本人相同,又不是本人的节点
		if((p->type == client->type) && (p->sockfd != client->sockfd) )
		{
			write(p->sockfd, client->logname,strlen(client->logname));
			write(p->sockfd," 说: ", 6);
			write(p->sockfd, buf, strlen(buf));
		}
		p= p->next;
	}
}
  1. 定义一个指针变量p,用于遍历链表。
  2. 使用while循环遍历链表,直到到达链表的末尾。
  3. 在循环内部,使用if语句判断当前节点是否满足广播条件,即类型与指定节点相同且不是指定节点本身。
  4. 如果满足条件,则使用write系统调用将指定节点的登录名、消息内容发送给当前节点。
  5. 继续遍历链表,直到到达链表的末尾。

需要注意的是,在调用multicastNode函数之前,需要确保链表中已经存在至少两个节点,并且指定节点的类型不为空。

 主函数

int main(int argc, char *argv[])
{
	char buf[1024]={0};
	int result,fd, newfd;
	fd_set rdset;
	pthread_t tid;
	struct sockaddr_in re;
	socklen_t addrlen = sizeof(re);
	
	int sockfd = init_socket(argv[1]);

	while(1)
	{
		newfd = accept(sockfd, (struct sockaddr *)&re,&addrlen);
		printf("newfd=%d\n", newfd);
		printf("IP:%s\n", inet_ntoa(re.sin_addr));
		pthread_create(&tid, NULL, talk2client, &newfd);
	}
	
}
  1. 定义一些变量:
    • char buf[1024]:用于存储接收到的数据或要发送的数据的缓冲区。
    • int result:用于存储接收或发送操作的结果。
    • int fd:文件描述符,未使用。
    • int newfd:用于存储新连接的套接字描述符。
    • fd_set rdset:用于存储要监视的文件描述符的集合。
    • pthread_t tid:用于存储新线程的ID。
    • struct sockaddr_in re:用于存储客户端的地址信息。
    • socklen_t addrlen:用于存储客户端地址信息的长度。
  2. 调用init_socket函数初始化服务器套接字,并将服务器的IP地址作为参数传递给它。init_socket函数返回一个套接字描述符sockfd,用于监听客户端的连接请求。
  3. 进入一个无限循环,不断接受客户端的连接请求并创建线程来处理。
    • 调用accept函数接受一个客户端的连接请求,并将新连接的套接字描述符存储在newfd中。同时,将客户端的地址信息存储在re中。
    • 打印出新连接的套接字描述符和客户端的IP地址。
    • 调用pthread_create函数创建一个新线程,并将talk2client函数的地址和newfd的地址作为参数传递给它。talk2client函数是用于处理客户端请求的函数,它将在新的线程中执行。
  4. 由于是无限循环,服务器将一直运行下去,不断接受新的客户端连接并创建线程来处理。

 线程实现

void *talk2client(void *arg)
{
	int result;
	int newfd = *(int *)arg;
	char * filename;
	char buf[200]={};
	//设置线程为 分离属性:自己回收 线程资源
	pthread_detach(pthread_self());
	ClientNode * p = (ClientNode *)malloc(sizeof(ClientNode));
	memset(p, 0, sizeof(ClientNode));
	memset(buf,0, sizeof(buf));
	// 1.填入:sockfd 
	p->sockfd = newfd;
	p->next = NULL;
	//2.读登录名
	write(newfd, name_msg, strlen(name_msg));
	result = recv(newfd, buf, sizeof(buf)-1, 0);
	buf[result-1] = 0; //去掉行尾的 '\n'
	printf("buf:%s\n", buf);
	strcpy(p->logname,buf);
	
	//3. 登录的类型
	write(newfd, menu, strlen(menu));
	result = recv(newfd, buf, sizeof(buf)-1, 0);
	if(result<=0)
	{
		printf("客户端断开连接\n");
		close(newfd);
		pthread_exit(NULL);
	}
	buf[result-1] = 0; //去掉行尾的 '\n'
	printf("buf:%s\n", buf);
	if(buf[0]=='1')
	{
		p->type = NEWS_TYPE;
		filename = "bbs_news.txt";
	}
	else if(buf[0]=='2')
	{
		p->type = ENTERTAINMENT_TYPE;
		filename = "bbs_trans.txt";
	}
	else if(buf[0]=='3')
	{
		p->type = SUPPLY_TYPE;
		filename = "bbs_fun.txt";
	}
	//插入客户结点到head链表
	pthread_rwlock_wrlock(&lockList);
	insertNode(p);
	pthread_rwlock_unlock(&lockList);

	//发送历史记录
	pthread_rwlock_rdlock(&lockFile);
	sendHistory(filename,newfd);
	pthread_rwlock_unlock(&lockFile);
	//进入主循环
	while(1)
	{
		result = recv(newfd, buf, sizeof(buf)-1, 0);
		if(result < 0)
		{
			perror("recv:");
			continue;
		}
		else if(result == 0)
		{
			printf("客户端断开连接\n");
			pthread_rwlock_wrlock(&lockList);
			DeleteNode(p);
			pthread_rwlock_unlock(&lockList);
			break;
		}
		else {
			if(strncmp(buf, "exit\n", 5)==0)
			{
				printf("客户端要主动离开\n");
				pthread_rwlock_wrlock(&lockList);
				DeleteNode(p);
				pthread_rwlock_unlock(&lockList);
				break;
			}
			//保存记录!
			buf[result -1]= 0; // 去掉 最后 \n 符号
			pthread_rwlock_wrlock(&lockFile);
			save2File(filename, buf);
			pthread_rwlock_unlock(&lockFile);

			// 多播信息

			pthread_rwlock_rdlock(&lockList);
			multicastNode(p, buf);
			pthread_rwlock_unlock(&lockList);
		}


	}
}

talk2client函数是一个处理与客户端通信的函数,它通过套接字与客户端进行数据的发送和接收。该函数被main函数中的pthread_create调用,在独立的线程中运行,以处理每个连接到服务器的客户端。

函数首先获取传递进来的参数,即新的套接字描述符newfd。然后,它创建一个ClientNode结构体的对象p,用于存储客户端的信息,如套接字描述符和登录名。接下来,函数与客户端进行交互,首先发送一个提示信息,要求客户端输入登录名,然后接收客户端的登录名并存储在p中。

接着,函数向客户端发送一个菜单,让客户端选择登录的类型,然后接收客户端的选择,根据选择设置p中的类型字段,并确定要保存消息的文件名。然后,函数将p插入到链表中,并发送历史记录给客户端。

最后,函数进入一个循环,不断接收客户端发送的消息。如果接收到的消息是"exit",则表示客户端主动断开连接,函数从链表中删除该客户端的信息并退出循环。否则,函数将消息保存到相应的文件中,并使用multicastNode函数将消息广播给其他类型相同的客户端。

需要注意的是,在函数中使用了读写锁来保护对共享资源(链表和文件)的访问,以避免并发访问导致的数据不一致问题。

如果有想自己试试的小伙伴们,为了方便代码我再单独放在下面

客户端代码全部

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

int init_socket(char *ip)
{
	//1. 创建一个套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	//2 定义一个 sockaddr_in 结构体,并初始化

	struct  sockaddr_in  serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(2000); //  h:host  n:network s : short
	inet_aton(ip, &serveraddr.sin_addr);

	//3. connect 服务器
	connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	
	
	return  sockfd;
}



int main(int argc, char *argv[])
{
	char buf[1024]={0};
	int result,fd;
	fd_set rdset;
	
	int sockfd = init_socket(argv[1]);
	while(1)
	{
		FD_ZERO(&rdset);
		FD_SET(sockfd, &rdset);
		FD_SET(0, &rdset);
		if(select(sockfd+1, &rdset, NULL, NULL, NULL)<0)
		{
			perror("select:");
			return -1;
		}
		if(FD_ISSET(sockfd,&rdset))//网络有数据可读
		{
			memset(buf,0,sizeof(buf));
			result = recv(sockfd, buf, sizeof(buf)-1, 0);
			if(result < 0)
			{
				perror("recv:");
				return -1;
			}
			else if(result == 0)
			{
				printf("服务器断开连接\n");
				break;
			}
			else{
				buf[result] = 0; //放 \0 作为buf的结束标志
				printf("%s\n",buf);
				
			}
		}
		if(FD_ISSET(0, &rdset)) //键盘有数据可读
		{
			memset(buf,0,sizeof(buf));
			if(fgets(buf, sizeof(buf), stdin))
			{

				write(sockfd,buf,strlen(buf));
				
			}
		}
	}
}

服务器代码全部

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

//定义类型枚举量
typedef enum ClientType
{
	NEWS_TYPE,
	ENTERTAINMENT_TYPE,
	SUPPLY_TYPE
}ClientType;

//定义客户节点量
typedef struct ClientNode
{
	int sockfd;
	enum ClientType type;
	char logname[20]; //登录名
	struct ClientNode * next;
}ClientNode;

ClientNode *head=NULL;
pthread_rwlock_t lockList = PTHREAD_RWLOCK_INITIALIZER;
pthread_rwlock_t lockFile = PTHREAD_RWLOCK_INITIALIZER;


char *name_msg = "请输入登录名:\n";
char *menu="\n"
" -------------------------\n"	
"      登录类型选择              \n"
"      1)  新闻              \n"
"      2)  娱乐              \n"
"      3)  交易              \n"
" -------------------------\n"	
"请选择登录的类目:";

int init_socket(char *ip)
{
	//1. 创建一个套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	//2 定义一个 sockaddr_in 结构体,并初始化

	struct  sockaddr_in  serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(2000); //  h:host  n:network s : short
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	// INADDR_ANY  有内核帮你找一个合适的网卡IP地址
	socklen_t addrlen = sizeof(serveraddr);
	int on=1;
	setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
	
	int r =  bind(sockfd,(struct sockaddr *)&serveraddr,addrlen);
	if(r < 0)
	{
		perror("bind:");
		return -1;
	}
	r = listen(sockfd, 5);
	if(r < 0)
	{
		perror("listen:");
		return -1;
	}
	
	return  sockfd;
}

//插入结点p到 Head链表中
void insertNode(ClientNode * p)
{
	p->next = head;

	head = p;
}
void DeleteNode(ClientNode *client)
{
	ClientNode *p = head;
	ClientNode *q = head;
	while(p)
	{
		if(p->sockfd == client->sockfd)
		{
			if(p == q)
			{
				//你要删除的是 第一个节点head
				head = p->next;
				
			}
			q->next = p->next;
			free(p);
			break;
		}
		q = p; //保存上一节点
		p = p->next; 
	}
}


void save2File(char *filename, char *buf)
{
	FILE *fp = fopen(filename,"a"); //append
	fputs(buf, fp);
	fclose(fp);

}
void sendHistory(char *filename,int newfd)
{
	char buf[1024]={};
	int n;
	FILE *fp = fopen(filename, "r");
	if(fp==NULL)
	{
		perror("fopen:");
		return ;
	}
	while(fgets(buf, sizeof(buf),fp))
	{
		write(newfd, buf, strlen(buf));
	}
	fclose(fp);
}

void multicastNode(ClientNode * client,char * buf)
{
	ClientNode *p = head;
	while(p)
	{
		//类型和本人相同,又不是本人的节点
		if((p->type == client->type) && (p->sockfd != client->sockfd) )
		{
			write(p->sockfd, client->logname,strlen(client->logname));
			write(p->sockfd," 说: ", 6);
			write(p->sockfd, buf, strlen(buf));
		}
		p= p->next;
	}
}

void *talk2client(void *arg)
{
	int result;
	int newfd = *(int *)arg;
	char * filename;
	char buf[200]={};
	//设置线程为 分离属性:自己回收 线程资源
	pthread_detach(pthread_self());
	ClientNode * p = (ClientNode *)malloc(sizeof(ClientNode));
	memset(p, 0, sizeof(ClientNode));
	memset(buf,0, sizeof(buf));
	// 1.填入:sockfd 
	p->sockfd = newfd;
	p->next = NULL;
	//2.读登录名
	write(newfd, name_msg, strlen(name_msg));
	result = recv(newfd, buf, sizeof(buf)-1, 0);
	buf[result-1] = 0; //去掉行尾的 '\n'
	printf("buf:%s\n", buf);
	strcpy(p->logname,buf);
	
	//3. 登录的类型
	write(newfd, menu, strlen(menu));
	result = recv(newfd, buf, sizeof(buf)-1, 0);
	if(result<=0)
	{
		printf("客户端断开连接\n");
		close(newfd);
		pthread_exit(NULL);
	}
	buf[result-1] = 0; //去掉行尾的 '\n'
	printf("buf:%s\n", buf);
	if(buf[0]=='1')
	{
		p->type = NEWS_TYPE;
		filename = "bbs_news.txt";
	}
	else if(buf[0]=='2')
	{
		p->type = ENTERTAINMENT_TYPE;
		filename = "bbs_trans.txt";
	}
	else if(buf[0]=='3')
	{
		p->type = SUPPLY_TYPE;
		filename = "bbs_fun.txt";
	}
	//插入客户结点到head链表
	pthread_rwlock_wrlock(&lockList);
	insertNode(p);
	pthread_rwlock_unlock(&lockList);

	//发送历史记录
	pthread_rwlock_rdlock(&lockFile);
	sendHistory(filename,newfd);
	pthread_rwlock_unlock(&lockFile);
	//进入主循环
	while(1)
	{
		result = recv(newfd, buf, sizeof(buf)-1, 0);
		if(result < 0)
		{
			perror("recv:");
			continue;
		}
		else if(result == 0)
		{
			printf("客户端断开连接\n");
			pthread_rwlock_wrlock(&lockList);
			DeleteNode(p);
			pthread_rwlock_unlock(&lockList);
			break;
		}
		else {
			if(strncmp(buf, "exit\n", 5)==0)
			{
				printf("客户端要主动离开\n");
				pthread_rwlock_wrlock(&lockList);
				DeleteNode(p);
				pthread_rwlock_unlock(&lockList);
				break;
			}
			//保存记录!
			buf[result -1]= 0; // 去掉 最后 \n 符号
			pthread_rwlock_wrlock(&lockFile);
			save2File(filename, buf);
			pthread_rwlock_unlock(&lockFile);

			// 多播信息

			pthread_rwlock_rdlock(&lockList);
			multicastNode(p, buf);
			pthread_rwlock_unlock(&lockList);
		}


	}
}


int main(int argc, char *argv[])
{
	char buf[1024]={0};
	int result,fd, newfd;
	fd_set rdset;
	pthread_t tid;
	struct sockaddr_in re;
	socklen_t addrlen = sizeof(re);
	
	int sockfd = init_socket(argv[1]);

	while(1)
	{
		newfd = accept(sockfd, (struct sockaddr *)&re,&addrlen);
		printf("newfd=%d\n", newfd);
		printf("IP:%s\n", inet_ntoa(re.sin_addr));
		pthread_create(&tid, NULL, talk2client, &newfd);
	}
	
}

记得先运行服务器再运行客户端哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1661318.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【科研】常用的实验结果评价指标(1) —— R2(R-square)是什么?

常用的实验结果评价指标&#xff08;1&#xff09; —— R2(R-square)&#xff0c;可能为负数吗&#xff1f;&#xff01; 提示&#xff1a;先说概念&#xff0c;后续再陆续上代码 文章目录 常用的实验结果评价指标&#xff08;1&#xff09; —— R2(R-square)&#xff0c;可能…

基于WPF的DynamicDataDisplay曲线显示

一、DynamicDataDisplay下载和引用 1.新建项目,下载DynamicDataDisplay引用: 如下图: 二、前端开发: <Border Grid.Row="0" Grid.Column="2" BorderBrush="Purple" BorderThickness="1" Margin="2"><Grid>…

【智能算法应用】基于麻雀搜索算法-支持向量回归预测(SSA-SVR)

目录 1.算法原理2.数学模型3.结果展示4.调试记录5.参考文献6.代码获取 1.算法原理 【智能算法】麻雀搜索算法&#xff08;SSA&#xff09;原理及实现 2.数学模型 支持向量机(SVM)是针对二分类问题&#xff0c;支持向量回归(SVR)基于SVM应用与回归问题。SVR回归与SVM分类的区…

【JVM】了解JVM规范中的虚拟机结构

目录 JVM规范的主要内容 1&#xff09;字节码指令集(相当于中央处理器CPU) JVM指令分类 2&#xff09;Class文件的格式 3&#xff09;数据类型和值 4&#xff09;运行时数据区 5&#xff09;栈帧 6&#xff09;特殊方法 7&#xff09;类库 JVM规范的主要内容 1&#…

Paddle 实现DCGAN

传统GAN 传统的GAN可以看我的这篇文章&#xff1a;Paddle 基于ANN&#xff08;全连接神经网络&#xff09;的GAN&#xff08;生成对抗网络&#xff09;实现-CSDN博客 DCGAN DCGAN是适用于图像生成的GAN&#xff0c;它的特点是&#xff1a; 只采用卷积层和转置卷积层&#x…

如何编译不同目录下的两个文件

1.直接编译 2.打包成动静态库进行链接

【Shell脚本】Shell编程之循环语句

目录 一.循环语句 1.for语句的结构 1.1.格式 1.2.实操案例 案例1. 案例2. 案例3. 案例4. 2.while语句的结构 2.1.格式 2.2.实操案例 案例1. 案例2. 案例3. 案例4. 3.until循环命令 3.1.格式 3.2.实操案例 案例1. 二.补充 1.常用转义符 一.循环语句 1.for…

鸿蒙内核源码分析(任务切换篇) | 看汇编如何切换任务

在鸿蒙的内核线程就是任务&#xff0c;系列篇中说的任务和线程当一个东西去理解. 一般二种场景下需要切换任务上下文: 在线程环境下&#xff0c;从当前线程切换到目标线程&#xff0c;这种方式也称为软切换&#xff0c;能由软件控制的自主式切换.哪些情况下会出现软切换呢? 运…

Leetcode—138. 随机链表的复制【中等】(cend函数)

2024每日刷题&#xff08;129&#xff09; Leetcode—138. 随机链表的复制 实现代码 /* // Definition for a Node. class Node { public:int val;Node* next;Node* random;Node(int _val) {val _val;next NULL;random NULL;} }; */class Solution { public:Node* copyRan…

【强训笔记】day18

NO.1 思路&#xff1a;双指针模拟。to_string将数字转化为字符。 代码实现&#xff1a; class Solution { public:string compressString(string param) {int left0,right0,nparam.size();string ret;while(right<n){while(right1<n&&param[right]param[right…

我在洛杉矶采访到了亚马逊云全球首席信息官CISO(L11)!

在本次洛杉矶举办的亚马逊云Re:Inforce全球安全大会中&#xff0c;小李哥作为亚马逊大中华区开发者社区和自媒体代表&#xff0c;跟着亚马逊云安全产品团队采访了亚马逊云首席信息安全官(CISO)CJ Moses、亚马逊副总裁Eric Brandwine和亚马逊云首席高级安全工程师Becky Weiss。 …

iOS--runloop的初步认识

runloop的初步认识 简单认识runloopEvent looprunloop其实就是个对象NSRunloop和CFRunLoopRef的依赖关系runloop与线程runloop moderunloop sourceCFRunLoopSourceCFRunLoopObserverCFRunLoopTimer runloop的实现runloop的获取添加ModeCFRunLoopAddCommonMode 添加Run Loop Sou…

找不到msvcp140.dll无法执行代码的原因分析及修复方法

当用户在尝试运行某些应用程序或游戏时&#xff0c;可能会遇到系统弹出错误提示&#xff0c;显示“找不到msvcp140.dll无法执行代码”这一错误信息&#xff0c;它会导致程序无法正常启动。为了解决这个问题&#xff0c;我经过多次尝试和总结&#xff0c;找到了以下五种解决方法…

【Linux】为什么有僵尸状态,什么是僵尸进程,造成危害以及如何避免“内存泄漏”问题详解

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …

C语言/数据结构——(相交链表)

一.前言 今天在力扣上刷到了一道题&#xff0c;想着和大家一起分享一下这道题——相交链表https://leetcode.cn/problems/intersection-of-two-linked-lists废话不多说&#xff0c;让我们开始今天的分享吧。 二.正文 1.1题目描述 是不是感觉好长&#xff0c;我也这么觉得。哈…

Ubuntu/Linux 安装Docker + PyTorch

文章目录 1. 提前准备2. 安装Docker2.1. 卸载冲突软件&#xff08;非必要&#xff09;2.2. 在Ubuntu系统上添加Docker的官方GPG密钥2.3. 将Docker的仓库添加到Ubuntu系统的APT源列表中2.4. 安装最新Docker2.5. 检查 3. 安装Nvidia Container Toolkit3.1. 在Ubuntu系统上添加官方…

WebRtc 视频通话,语音通话实现方案

先了解一下流程 和 流程图(chatGpt的回答) 实现 (底层代码实现, 可作为demo熟悉) 小demo <template><div><video ref"localVideo" autoplay muted></video> <!-- 本地视频元素&#xff0c;用于显示本地视频 --><video ref"r…

vivado 配置存储器支持-Artix-7 配置存储器器件

配置存储器支持 本章主要讲解 Vivado 软件支持的各种非易失性器件存储器。请使用本章作为指南 &#xff0c; 按赛灵思系列、接口、制造商、 密度和数据宽度来为您的应用选择适用的配置存储器器件。 Artix-7 配置存储器器件 下表所示闪存器件支持通过 Vivado 软件对 A…

布局全球内容生态,酷开科技Coolita AIOS以硬核品质亮相

当前&#xff0c;全球产业链供应链格局持续重构&#xff0c;成为影响中国对外经济发展的重要因素。2024年4月15至5月5日&#xff0c;历史久、规模大、层次高&#xff0c;作为中国外贸风向标的第135届中国进出口商品交易会&#xff08;即广交会&#xff09;在美丽的广州隆重举行…