BBS客户端服务器的编写

根据网络编程中的内容,我们本篇文章将讲解一个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);
	}
	
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32MP157_程序烧录

STM32MP157_程序烧录 说明&#xff1a; 1、使用emmc作为存储媒介&#xff0c;emmc是核心板上的存储颗粒空间有8GB 2、SD卡作为存储媒介&#xff0c;底板上有SD卡的插槽 emmc方式 软件&#xff1a;烧录软件使用STM32CubeProgrammer 连接线&#xff1a;硬件连接线使用type_c数据线…

RTSP/Onvif安防监控系统EasyNVR级联视频上云系统EasyNVS报错“Login error”的原因排查与解决

EasyNVR安防视频云平台是旭帆科技TSINGSEE青犀旗下支持RTSP/Onvif协议接入的安防监控流媒体视频云平台。平台具备视频实时监控直播、云端录像、云存储、录像检索与回看、告警等视频能力&#xff0c;能对接入的视频流进行处理与多端分发&#xff0c;包括RTSP、RTMP、HTTP-FLV、W…

jenkins使用gitLab(极狐)认证登陆

jenkins安装 GitLab Authentication插件 我因为java版本和最新GitLab Authentication 1.19版本不兼容&#xff0c;选择了本地安装 找个历史版本1.13版本&#xff0c;然后下载到电脑上 - 本地上传插件并安装 在极狐上创建一个应用 - 配置应用信息 应用名&#xff1a;jenkinsLo…

2024年最新方法下载钉钉群直播回放

链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;1234 --来自百度网盘超级会员V10的分享 1.首先解压好所有的压缩包&#xff0c;这个压缩包里面还套着一共逍遥一仙下载器压缩包&#xff0c;也解压 2.进入逍遥一仙下载器文件夹&#xff0c;打开M3U8 V1.4.8 0508.e…

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

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

宏集Panorama SCADA软件获BACnet BTL认证

Panorama 获得BACnet BTL认证 建筑物的组件&#xff08;空调系统、照明传感器等&#xff09;能否使用共同通讯协议&#xff1f;这正是标准化 BACnet协议&#xff08;Building Automation and Control Networks&#xff09;所提供的功能。该协议旨在实现建筑物中各种设备和系统…

初探 JUC 并发编程:读写锁 ReentrantReadWriteLock 原理(8000 字源码详解)

本文中会涉及到一些前面 ReentrantLock 中学到的内容&#xff0c;先去阅读一下我关于独占锁 ReentrantLock 的源码解析阅读起来会更加清晰。 初探 JUC 并发编程&#xff1a;独占锁 ReentrantLock 底层源码解析 6.4&#xff09;读写锁 ReentrantReadWriteLock 原理 前面提到的 R…

谈基于ATTCK框架的攻击链溯源

引言 网络安全在当今数字化时代变得尤为关键&#xff0c;而MITRE公司开发的ATT&CK框架则成为了安全专业人员的重要工具。ATT&CK是一种广泛使用的攻击行为分类和描述框架。其目的在于提供一个共同的语言&#xff0c;使安全专业人员能够更好地理解攻击者的行为和目标&…

整理好了!咸阳市各区县高新技术企业申报奖补标准,高企认定时间流程及申报条件

咸阳市及各区县高企申报奖励 咸阳市&#xff1a;对首次通过认定的高新技术企业给予20万元的奖励&#xff0c;通过复审的企业给予5万元奖励。政策依据&#xff1a;咸阳市人民政府办公室关于印发《咸阳市科技型企业三年倍增计划实施方案&#xff08;2022—2024年&#xff09;》的…

如何在您的WordPress网站上安装和设置W3 Total Cache

本周有一个客户&#xff0c;购买Hostease的虚拟主机&#xff0c;询问我们的在线客服&#xff0c;如何在您的WordPress网站上安装和设置W3 Total Cache&#xff1f;我们为用户提供相关教程&#xff0c;用户很快解决了遇到的问题。在此&#xff0c;我们分享这个操作教程&#xff…

【2022 深圳 ArchSummit 】大数据架构稳定性保障实践

文章目录 一、前言二、现状三、大数据架构的历史变迁&#xff08;一&#xff09;洪荒期&MR&#xff08;二&#xff09;远古期&MPP&#xff08;四&#xff09;近现代&Flink/Spark&#xff08;五&#xff09;现如今&实时数据湖架构 四、架构稳定的关键因素&#…

学习100个Unity Shader (17) --- 深度纹理

文章目录 效果shader部分C# 部分理解参考 效果 shader部分 Shader "Example/DepthTexture" {SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"sampler2D _CameraDepthTexture;struct a2v{float4 pos : POSITIO…

公司活动想找媒体报道宣传怎样联系媒体?

作为公司宣传负责人,我深知媒体报道对于企业活动宣传的重要性。然而,在过去,每当有重要活动需要媒体曝光时,我总会被繁琐的媒体联系工作所困扰。 那时,我需要一家家地查询媒体联系方式,发送邮件、打电话,甚至亲自前往媒体机构进行沟通。然而,这样的过程不仅费时费力,而且效率低…

Linux系统调用过程详解:应用程序调用驱动过程

Linux下应用程序调用驱动程序过程&#xff1a; &#xff08;1&#xff09;加载一个驱动模块(.ko)&#xff0c;产生一个设备文件&#xff0c;有唯一对应的inode结构体 a、每个设备文件都有一个对应的’inode‘结构体&#xff0c;包含了设备的主次设备号&#xff0c;是设备的唯一…

ChatGLM3-6B部署与微调及微调后使用

记录ChatGLM3-6B部署及官方Lora微调示例详细步骤及如何使用微调后的模型进行推理 一、下载代码 使用git clone 命令下载源码 git clone https://github.com/THUDM/ChatGLM3.git 如图所示 二、下载模型 模型权重文件从魔塔进行下载&#xff0c;不需要翻墙。权重文件比较大&…

搭建知识库必备:12个开源 Wiki 软件工具盘点

在任何成功的公司中&#xff0c;部门间的知识共享是至关重要的。如果没有一个简单的信息交流方法&#xff0c;团队怎样才能有效合作呢&#xff1f;Wiki软件提供了一种创建、组织及在全公司范围内分享知识的直接方法。但是&#xff0c;哪一种Wiki软件是最佳的选择呢&#xff1f;…

【计算机毕业设计】springboot工资管理系统

人类现已迈入二十一世纪&#xff0c;科学技术日新月异&#xff0c;经济、资讯等各方面都有了非常大的进步&#xff0c;尤其是资讯与 网络技术的飞速发展&#xff0c;对政治、经济、军事、文化等各方面都有了极大的影响。 利用电脑网络的这些便利&#xff0c;发展一套工资管理系…

Unity 修复Sentinel key not found (h0007)错误

这个问题是第二次遇到了&#xff0c;上次稀里糊涂的解决了&#xff0c;也没当回事&#xff0c;这次又跑出来了&#xff0c;网上找的教程大部分都是出自一个人。 1.删除这个路径下的文件 C:\ProgramData\SafeNet Sentinel&#xff0c;注意ProgramData好像是隐藏文件 2.在Windows…

Mac安装激活--Typora,一个比记事本更加强大的纯文本软件

一、安装 1.首先到官网下载Mac版的Typora,下载地址&#xff1a;https://typoraio.cn/ &#xff08;1&#xff09;打开默认中文站 &#xff08;2&#xff09;往下滑&#xff0c;下载Mac版 2.下载完成后&#xff0c;会看到Typora.dmg文件&#xff0c;点击打开文件 3.打开Typ…

mac苹果电脑卡顿反应慢如何解决?2024最新免费方法教程

苹果电脑以其稳定的性能、出色的设计和高效的操作系统&#xff0c;赢得了广大用户的喜爱。然而&#xff0c;随着时间的推移&#xff0c;一些用户会发现自己的苹果电脑开始出现卡顿、反应慢等问题。这不仅影响使用体验&#xff0c;还会影响工作效率。那么&#xff0c;面对这些问…