基于linux的BBS电子公告栏设计

项目简介:

运用TCP/UDP网络通信协议,实现了局域网内多人聊天和文件传输的功能。

服务器端:利用线程与用户交互,其中利用链表管理已连接的用户节点,能够根据用户的登陆类型进行相应信息的广播多播和文件的读写,同时具备用户上线和下线提醒机制。并且能实时统计在线人数。

客户端:连接服务器,使用select监听多个io文件(键盘和网络),以此收发信息。

程序流程:

服务器:

在主控程序中监听并接受客户端的连接申请,创建线程与已连接的客户交互:

1.发送选择菜单(新闻,娱乐,交易),等待用户选择要登录的公告栏的类型。用户可选择进入或退出,用户选中后,将用户节点加入链表。

2.根据用户选择的公告栏类型,发送相应公告栏中的信息,同时等待用户发布消息。

3.若用户中途退出,则链表中删除该用户节点。

4.接收到用户新发布的消息后,保存新消息到相应公告栏,同时转发消息到已登录该公告栏的其他用户(多播)。

 客户端:

连接服务器,在主循环中利用select多路复用函数监听键盘、网络 io,接收和发送数据。

核心代码:

服务器端:

首先介绍线程中用于交互的几个模块:

1.定义了枚举类型变量ClientType,对应菜单中的不同的公告栏,供用户选择。

typedef enum ClientType
{
	NEWS_TYPE,
	ENTERTAINMENT_TYPE,
	SUPPLY_TYPE
}ClientType;

char *menu="\n"
"--------------------\n"
"-----公告栏类型选择---\n"
"-------1.新闻-------\n"
"-------2.娱乐-------\n"
"-------3.交易-------\n"
"--------------------\n"
"请选择登录的类别----\n";

 2.定义客户端节点结构体,用于保存用户信息。

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

3.链表中插入和删除客户节点

ClientNode *head = NULL;

//插入结点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; 
	}
}

4.发送相应公告栏中的信息给用户。

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

5.保存用户新发布的消息到相应公告栏

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

}

6.转发新消息给该公告栏中已登录的其他用户

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.创建服务器地址结构体,使用socket函数创建监听套接字sockfd,并设置sockfd可以多次绑定服务器端口port,使用bind函数将sockfd绑定服务器地址,再使用listen函数监听客服端的连接请求。

int init_server(char *ip)
{
	int sockfd;
	//创建保存服务器地址信息的结构体
	struct sockaddr_in saddr;
	saddr.sin_family= AF_INET;//协议族
	saddr.sin_port= htons(2002);//端口
	saddr.sin_addr.s_addr = inet_addr(ip);//ip
	//INADDR_ANY;//由内核来分配一个合适网卡IP

	//1. 创建套接字 
	sockfd = socket(AF_INET, SOCK_STREAM,0);
	
	//设置sockfd可以重复使用port  
	int on=1;
	int ret  = setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT,&on,sizeof(on));
	printf("ret = %d\n", ret);
	//2. bind 
	if(bind(sockfd, (struct sockaddr*)&saddr,sizeof(saddr))){
		perror("bind");
		return -1;
	}
	//3. listen
	listen(sockfd, 10);
	return sockfd;
}

2.创建线程talk2client 用于与客户端交互。 其中在插入、删除客户节点InsertNode(),DeleteNode()、发送信息SendHistory()、保存新消息Save2File()、转发新消息MulticastNode()等操作时都运用了线程锁,通过锁定读写锁、操作、释放锁的方式,确保在操作时保持数据结构的一致性。

void *talk2client(void *arg)
{
	int connfd = *(int *)arg;
	int result;
	char buf[200];
	char *filename;

	
	printf("connfd = %d\n", connfd);
	//分配一个新的节点Node
	ClientNode *p = (ClientNode*)malloc(sizeof(ClientNode));
	memset(p, 0, sizeof(ClientNode));
	memset(buf,0, sizeof(buf));
	//1. 填入 sockfd 
	p->sockfd = connfd; 
	p->next = NULL;

	//2. 读登录名
	write(connfd, "请输入登录名", strlen("请输入登录名"));
	result = read(connfd, buf, sizeof(buf));
	//result = recv(connfd, buf, sizeof(buf), 0);
	printf("用户:%s 已登陆\n ", buf);
	strcpy(p->logname, buf);
	memset(buf,0, sizeof(buf));

	// 3.登录的类型
	write(connfd, menu, strlen(menu));
	//result = read(connfd, buf, sizeof(buf));
	result = recv(connfd, buf, sizeof(buf), 0);
	if(result <=0) 
	{
		printf("客户端断开连接\n");
		close(connfd);
		pthread_rwlock_wrlock(&flag_rwlock);
		flag--;
		pthread_rwlock_unlock(&flag_rwlock);
		pthread_exit(NULL);
	}
	printf("登录类型:%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_fun.txt";
	}
	else if(buf[0]=='3')
	{
		p->type = SUPPLY_TYPE;
		filename = "bbs_trans.txt";
	}

	// 插入客户节点到 head 链表
	pthread_rwlock_wrlock(&lockList);
	InsertNode(p); 
	pthread_rwlock_unlock(&lockList);
	// 发送历史记录
	pthread_rwlock_rdlock(&lockFile);
	SendHistory(filename, connfd);
	pthread_rwlock_unlock(&lockFile);

	//进入主循环
	while(1)
	{
		//等待客户发布消息
		memset(buf,0, sizeof(buf));
		result = recv(connfd, buf, sizeof(buf),0);
		if(result < 0)
		{
			perror("recv");
			continue;
		}
		else if(result == 0)
		{
			printf("%s 用户断开连接\n",p->logname);
			pthread_rwlock_wrlock(&lockList);
			DeleteNode(p); 
			pthread_rwlock_unlock(&lockList);
			pthread_rwlock_wrlock(&flag_rwlock);
			flag--;
			pthread_rwlock_unlock(&flag_rwlock);
			break;
		}
		else
		{
			if(strncmp(buf,"exit",5)==0)
			{
				printf("%s 用户主动离开\n",p->logname);
				pthread_rwlock_wrlock(&lockList);
				DeleteNode(p); 
				pthread_rwlock_unlock(&lockList);
				pthread_rwlock_wrlock(&flag_rwlock);
				flag--;
				pthread_rwlock_unlock(&flag_rwlock);
				break;
			}
		//保持记录到 对应的文件
			pthread_rwlock_wrlock(&lockFile);
			Save2File(filename, buf);
			pthread_rwlock_unlock(&lockFile);
		//多播信息
			pthread_rwlock_rdlock(&lockList);
			MulticastNode(p, buf);
			pthread_rwlock_unlock(&lockList);
		}
	}
}

3.主函数中监听和接受客户端连接,并实时统计在线用户数。同时创建线程与客户交互。此外,主函数用到int argc, char *argv[]作为参数,argv[1]作为init_server()实参,实现了通过命令行输入服务器ip。

int main(int argc, char *argv[])
{
	struct sockaddr_in clientaddr;//用于保存客户端地址
	pthread_t pthread1;
	
	int addrlen = sizeof(clientaddr);
	printf("正在等待客户端连接......");
	int sockfd = init_server(argv[1]);
	int connfd;
	while(1)
	{
		connfd = accept(sockfd, (struct sockaddr*)&clientaddr, &addrlen);
		if(connfd!=NULL)
		{
			flag++;
		}
		printf("%d 个客户已连接\n", flag);
		printf("%s connect server\n", inet_ntoa(clientaddr.sin_addr));
		printf("IP:%s\n", inet_ntoa(clientaddr.sin_addr));
		//创建一个线程 用于与客户端交互
		pthread_create(&pthread1, NULL, talk2client, &connfd);
	}
}

客户端: 

1.使用socket()创建套接字用于与服务器通信,并使用connect()函数连接服务器。

int init_socket(char *ip)
{
	int sockfd;
	struct sockaddr_in saddr;
	saddr.sin_family= AF_INET;
	saddr.sin_port= htons(2002);
	saddr.sin_addr.s_addr = inet_addr(ip);

	//1. 创建套接字 
	sockfd = socket(AF_INET, SOCK_STREAM,0);

	//2. 连接服务器
	if(connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)))
	{
		perror("connect");
		return -1;
	}
	return sockfd;
}

2.主函数中使用select多路复用函数监听键盘和网络的数据,并进行收发。客户端与服务器端一致,实现了通过命令行输入服务器ip。

int main(int argc, char *argv[])
{
	int sockfd = init_socket(argv[1]);
	fd_set rdset;
	int r,n;
	char buf[1024];
	
	while(1)
	{
		FD_ZERO(&rdset);
		FD_SET(0, &rdset); //键盘
		FD_SET(sockfd, &rdset);//网络 
	
		r = select(sockfd+1, &rdset, NULL, NULL, NULL);
		if(FD_ISSET(0, &rdset)) 
		{
			memset(buf,0,sizeof(buf));
			fgets(buf, sizeof(buf),stdin);
			send(sockfd, buf, strlen(buf)-1, 0);
			if(!strcmp(buf,"quit\n"))
			{
				close(sockfd);
				return 0;
			}
				
		}
		else if(FD_ISSET(sockfd, &rdset)) 
		{
			memset(buf,0,sizeof(buf));
			n = recv(sockfd, buf,sizeof(buf),0 );
			printf("%s\n", buf);
		}
	}
}

项目演示:

项目部分功能演示:

1.用户Tom,Jerry,连接服务器。服务器实现上线提醒,实时统计在线人数。

2.用户选择公告栏类型,服务器向用户发送相应公告栏中的信息。

3.用户在公告栏发布消息,服务器实现保存并多播。

ps:考虑到文章过长,这里我仅展示该项目的一部分功能。 如有更好的设计思路,欢迎大家批评指正和优化设计。

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值