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 : shortinet_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 : shortserveraddr.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){//你要删除的是 第一个节点headhead = 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 : shortinet_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 : shortserveraddr.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){//你要删除的是 第一个节点headhead = 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"); //appendfputs(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.mzph.cn/diannao/9789.shtml

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

相关文章

Java解决垂直鉴权问题(对垂直权限进行校验)

Java解决垂直鉴权问题&#xff08;对垂直权限进行校验&#xff09; 文章目录 Java解决垂直鉴权问题&#xff08;对垂直权限进行校验&#xff09;前言一、垂直鉴权是什么&#xff1f;二、实现过程1.新建接口权限菜单映射表2.项目初始化时加载接口菜单映射关系3.自定义过滤器拦截…

[C++][数据结构]哈希2:开散列/哈希桶的介绍和简单实现

前言 接着上一篇文章&#xff0c;我们知道了闭散列的弊端是空间利用率比较低&#xff0c;希望今天学习的开散列可以帮我们解决这个问题 引入 开散列法又叫链地址法(开链法)&#xff0c;首先对关键码集合用散列函数计算散列地址**&#xff0c;具有相同地址的关键码归于同一子…

latex algorithm2e 库学习总结

案例1 \documentclass{article}\usepackage{xeCJK} \usepackage[]{algorithm2e} %\usepackage{ctex} % 中文包\begin{document}\renewcommand{\algorithmcfname}{算法} % 把标题设置为“算法” \begin{algorithm…

云衔科技成为卓豪Zoho中国区代理商,开启智能化企业管理新篇章

每一家企业数字化转型&#xff0c;都在寻求通过技术创新实现业务的飞跃。为了更好地服务于中国企业的数字化转型需求&#xff0c;云衔科技荣幸宣布正式成为卓豪Zoho中国区代理商&#xff0c;这一强强联合将为市场带来全新的数字化解决方案与服务体验&#xff0c;共同开启中国企…

鲁教版六年级数学上册-笔记

文章目录 第一章 丰富的图形世界1 生活中的立体图形2 展开和折叠3 截一个几何体4 从三个方向看物体的形状 第二章 有理数及其运算1 有理数2 数轴3 绝对值4 有理数的加法5 有理数的减法6 有理数的加减混合运算7 有理数的乘法8 有理数的除法9 有理数的乘方10 科学计数法11 有理数…

顺序表经典算法OJ题-- 力扣27,88

题1&#xff1a; 移除元素 题2&#xff1a; 合并两个有序数组 一&#xff1a;题目链接&#xff1a;. - 力扣&#xff08;LetCode&#xff09; 思路&#xff1a;&#xff08;双指针法&#xff09; 创建两个变量src&#xff0c;dst 1&#xff09;若src指向的值为val&#xf…

leetcode-字符串的排列-100

题目要求 思路 1.因为只涉及到字符&#xff0c;因此可以进行排序 2.创建临时字符串&#xff0c;当临时字符串temp的长度等于str的长度&#xff0c;作为判出条件。 3.创建一个标记的数组&#xff0c;每次在temp中插入一个字符&#xff0c;便在对应的数组下标设置为1&#xff0c…

国内如何访问 OpenAI 的 api

这个问题甚至我的一些大厂的朋友也不太清楚&#xff0c;所以我觉得有必备写一篇文章来简单盘盘它&#xff0c;希望能帮助到有需要的人 众所周知&#xff0c;由于大陆与 OpenAI 双方互相封锁&#xff0c;大陆是无法直接访问 OpenAI api 的 不过由于 GPT 4 的统治地位&#xff0c…

C++|二叉搜索树

一、二叉搜索树的概念 二叉搜索树又称为二叉排序树&#xff0c;它或者是一颗空树&#xff0c;或者是具有以下性质的二叉树&#xff1a; 若它的左子树不为空&#xff0c;则左子树上所有节点的值小于根节点的值若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根结…

Obsidian 下载安装和运行

1 官网页面 2 Github 页面 3 选择合适的版本&#xff0c;下载后运行。 附录&#xff1a; 官网&#xff1a; https://obsidian.md/ Github 地址&#xff1a; https://github.com/obsidianmd/obsidian-releases/releases 参考&#xff1a; Markdown 官方教程 https://markdow…

2024 年 数维杯(B题)大学生数学建模挑战赛 | 生物质和煤共热解 | 数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 CS团队倾注了大量时间和心血&#xff0c;深入挖掘解决方案。通…

什么是FMEA的分析范围?——FMEA软件

免费试用FMEA软件-免费版-SunFMEA FMEA的分析范围广泛而深入&#xff0c;涵盖了产品设计、制造过程、供应链管理以及使用和维修等多个方面。 产品设计是FMEA分析的重要一环。在设计阶段&#xff0c;FMEA能够帮助工程师识别潜在的设计缺陷&#xff0c;并预测这些缺陷可能对产品…

【系统架构师】-案例篇(七)信息安全

某软件公司拟开发一套信息安全支撑平台&#xff0c;为客户的局域网业务环境提供信息安全保护。该支撑平台的主要需求如下&#xff1a; 1.为局域网业务环境提供用户身份鉴别与资源访问授权功能&#xff1b; 2.为局域网环境中交换的网络数据提供加密保护&#xff1b; 3.为服务…

综述列表(~2024.05.10)

&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 每周末更新&#xff0c;完整版进群获取。 Q 群在群文件&#xff0c;VX 群每周末更新。

多线程-写入读取文件,使用同步逻辑

在一个进程中&#xff0c;创建一个子线程。 主线程负责:向文件中写入数据 子线程负责:从文件中读取数据 要求使用线程的同步逻辑&#xff0c;保证一定在主线程向文件中写入数据成功之后&#xff0c;子线程才开始运行&#xff0c;去读取文件中的数据 #include <stdio.h> …

Linux0.11中MINIX 文件系统

阅读linux 的源码的时候对minix 文件系统有很多的疑惑&#xff0c;根据自己的认识将这些做一个总结。 MINIX 文件系统由六个部分组成&#xff0c;分别是引导块&#xff0c;超级块&#xff0c;i结点位图&#xff0c;逻辑块位图&#xff0c;i结点&#xff0c;数据块。 引导块&am…

部署xwiki服务需要配置 hibernate.cfg.xml如何配置?

1. 定位 hibernate.cfg.xml 文件 首先&#xff0c;确保您可以在 Tomcat 的 XWiki 部署目录中找到 hibernate.cfg.xml 文件&#xff1a; cd /opt/tomcat/latest/webapps/xwiki/WEB-INF ls -l hibernate.cfg.xml如果文件存在&#xff0c;您可以继续编辑它。如果不存在&#xff…

使用GitLab自带的CI/CD功能在远程服务器部署项目(三)

前置内容&#xff1a; 通过Docker Compose部署GitLab和GitLab Runner&#xff08;一&#xff09; 使用GitLab自带的CI/CD功能在本地部署项目&#xff08;二&#xff09; 目录 一、在GitLab服务器上生成私钥与公钥 二、将公钥拷贝到应用服务器上 三、将私钥给到Docker Exec…

深入解析Java中Set接口

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

数据驱动实战二

目标 掌握数据驱动的开发流程掌握如何读取JSON数据文件巩固PO模式 1. 案例 对TPshop网站的登录模块进行单元测试 1.1 实现步骤 编写测试用例采用PO模式的分层思想对页面进行封装编写测试脚本定义数据文件&#xff0c;实现参数化 1.2 用例设计 1.3 数据文件 {"login…