项目简介:
运用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:考虑到文章过长,这里我仅展示该项目的一部分功能。 如有更好的设计思路,欢迎大家批评指正和优化设计。