此网络聊天室使用UDP协议,来实现聊天室功能。其中,服务端可以记录各客户端的地址,服务端也会将客户端发送的消息转发给各客户端。
共有3种消息类型:
登录:服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
退出:服务器删除退出客户端的地址,并把退出消息发送给其它客户端。
此项目使用单向链表来存储各登录用户的地址和端口信息,采用的结构体如下:
typedef struct node
{
struct sockaddr_in addr;
struct node *next;
} node, *node_t;
typedef struct msg
{
int type; //L C Q
char name[32]; //用户名
char text[128]; //消息正文
} msg;
其中type表示消息类型,当type=L时,代表登录、type=C,代表发送消息、type=Q,代表用户退出;第一个结构体的addr则用来存储登录用户的IP地址和端口信息。
- 对于用户端,首先创建套接字文件,该套接字文件采用IPv4的通信方式;创建完套接字后开始填充其IP地址、端口、通信方式等信息;填充完成后将其登录信息发送到客户端,然后通过多进程来进行发送信息和接收信息,其中子进程用来发送消息,父进程用来接收消息,当输入quit时将会退出系统。
用户端的具体代码如下:
#include <stdio.h>
#include "8-chatroom.h"
void quit_handler(int sig)
{
kill(getpid(), SIGKILL);
}
int main(int argc, char const *argv[])
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
if (argc != 3)
{
printf("please input ./11-chatroom <port>\n");
return -1;
}
struct sockaddr_in serveraddr, caddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t len = sizeof(caddr);
char buf[128];
msg usr;
printf("请输入用户名:");
fgets(usr.name, sizeof(usr.name), stdin);
usr.type = 'L';
sendto(sockfd, &usr, sizeof(usr), 0, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
pid_t pid = fork();
if (pid < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0)
{
while (1)
{
fgets(usr.text, sizeof(usr.text), stdin);
usr.type = 'C';
if (usr.text[strlen(usr.text) - 1] == '\n')
usr.text[strlen(usr.text) - 1] = '\0';
sendto(sockfd, &usr, sizeof(usr), 0,
(struct sockaddr *)&serveraddr, sizeof(serveraddr));
if (strncmp(usr.text, "quit", 4) == 0)
{
usr.type = 'Q';
sendto(sockfd, &usr, sizeof(usr), 0,
(struct sockaddr *)&serveraddr, sizeof(serveraddr));
break;
}
}
}
else
{
int recvbyte;
while (1)
{
signal(SIGCHLD, quit_handler);//子进程状态发生变化时,回收子进程
recvbyte = recvfrom(sockfd, &usr, sizeof(usr), 0,
(struct sockaddr *)&caddr, &len);
if (recvbyte < 0)
{
perror("recvfrom err");
return -1;
}
if (usr.type == 'L')//接收其他用户的上线信息
{
if (usr.name[strlen(usr.name) - 1] == '\n')
usr.name[strlen(usr.name) - 1] = '\0';
printf("name:%s上线\n", usr.name);
}
if (usr.type == 'C')//接收其他用户发送的消息
{
if (usr.name[strlen(usr.name) - 1] == '\n')
usr.name[strlen(usr.name) - 1] = '\0';
printf("%s:%s\n", usr.name, usr.text);
}
if (usr.type == 'Q')//接收其他用户下线的信息
{
if (usr.name[strlen(usr.name) - 1] == '\n')
usr.name[strlen(usr.name) - 1] = '\0';
printf("name:%s下线\n", usr.name);
}
}
wait(NULL);
exit(0);
}
close(sockfd);
return 0;
}
服务端同样创建套接字文件、信息填充、绑定套接字,但父进程采用了多线程的方式来接收消息和发送消息,子线程用来服务端发送消息。具体流程可观看本人分享的代码。
其具体代码如下:
#include <stdio.h>
#include "8-chatroom.h"
struct sockaddr_in serveraddr, caddr;
void *handler(void *arg);
void login(int sockfd, node_t H, msg m, struct sockaddr_in caddr);
void send_news(int sockfd, node_t H, msg m, struct sockaddr_in caddr);
void quit(int sockfd, node_t H, msg m, struct sockaddr_in caddr);
int main(int argc, char const *argv[])
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err");
return -1;
}
if (argc != 2)
{
printf("please input ./11-chatroom <port>\n");
return -1;
}
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(caddr);
if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("bind err");
return -1;
}
pthread_t tid;
pthread_create(&tid, NULL, handler, &sockfd);
pthread_detach(tid);
node_t H = head_create();//创建头节点
msg usr;
int recvbyte;
while (1)
{
recvbyte = recvfrom(sockfd, &usr, sizeof(usr), 0,
(struct sockaddr *)&caddr, &len);
if (recvbyte < 0)
{
perror("recvfrom err");
return -1;
}
if (usr.type == 'L')//用户登录时
{
login(sockfd, H, usr, caddr);
}
if (usr.type == 'C')//用户发送消息时
{
send_news(sockfd, H, usr, caddr);
}
if (usr.type == 'Q')//用户退出时
{
quit(sockfd, H, usr, caddr);
}
}
close(sockfd);
return 0;
}
//有用户登录时,储存其ip地址,并发送
void login(int sockfd, node_t H, msg m, struct sockaddr_in caddr)
{
node_t p = H->next;
while (p != NULL)
{
sendto(sockfd, &m, sizeof(m), 0,
(struct sockaddr *)&(p->addr), sizeof(p->addr));
p = p->next;
}
tail_insert(H, caddr);//此函数是链表的尾插函数
if (m.name[strlen(m.name) - 1] == '\n')
m.name[strlen(m.name) - 1] = '\0';
printf("ip:%s port:%d name:%s登录\n", inet_ntoa(caddr.sin_addr),
ntohs(caddr.sin_port), m.name);
}
//接收消息,并转发
void send_news(int sockfd, node_t H, msg m, struct sockaddr_in caddr)
{
node_t p = H->next;
while (p != NULL)
{
if (memcmp(&(p->addr), &caddr, sizeof(caddr)) == 0)
{
p = p->next;
continue;
}
sendto(sockfd, &m, sizeof(m), 0,
(struct sockaddr *)&(p->addr), sizeof(p->addr));
p = p->next;
}
if (m.name[strlen(m.name) - 1] == '\n')
m.name[strlen(m.name) - 1] = '\0';
printf("%s:%s\n", m.name, m.text);
}
//用户退出
void quit(int sockfd, node_t H, msg m, struct sockaddr_in caddr)
{
node_t p = H->next;
while (p != NULL)
{
sendto(sockfd, &m, sizeof(m), 0,
(struct sockaddr *)&(p->addr), sizeof(p->addr));
p = p->next;
}
if (m.name[strlen(m.name) - 1] == '\n')
m.name[strlen(m.name) - 1] = '\0';
printf("ip:%s port:%d name:%s已退出\n", inet_ntoa(caddr.sin_addr),
ntohs(caddr.sin_port), m.name);
local_delete(H, caddr);//此函数是链表的按位置删除函数
}
//服务端发送消息
void *handler(void *arg)
{
msg m;
int sockfd = *((int *)arg);
m.type = 'C';
strcpy(m.name, "服务器");
while (1)
{
fgets(m.text, sizeof(m.text), stdin);
if (m.name[strlen(m.name) - 1] == '\n')
m.name[strlen(m.name) - 1] = '\0';
sendto(sockfd, &m, sizeof(m), 0,
(struct sockaddr *)&serveraddr, sizeof(serveraddr));
}
pthread_exit(NULL);
}
完成后的效果展示:
当然,本项目只能用于局域网之间的通信,本次分享到这里就结束了,下次再见!