利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
关于如何保存客户端的地址结构,这里我用的是链表。
链表节点结构体:
struct node{
struct sockaddr_in addr;
struct node *next;
};
消息对应的结构体(同一个协议)
typedef struct msg_t
{
int type;//L C Q
char name[32];//用户名
char text[128];//消息正文
}MSG_t;
- 客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。
这里给出流程图
这里根据流程图内容将代码写出,代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#define POST 7598
void handler();
typedef struct msg_t
{
char type; //L登录 C聊天 Q退出
char name[32]; //用户名
char text[128]; //消息正文
} MSG;
int main(int argc, char const *argv[])
{
int lfd, cfd, ret, m;
int shu = 0;
MSG c, s;
//创建套接字
lfd = socket(AF_INET, SOCK_DGRAM, 0);
if (lfd == -1)
perror("socket error");
//初始化地址结构
struct sockaddr_in caddr;
caddr.sin_family = AF_INET;
caddr.sin_port = htons(POST);
caddr.sin_addr.s_addr = inet_addr("172.20.10.12");
socklen_t clen;
clen = sizeof(caddr);
pid_t pid;
//设置信号捕捉函数,回收子进程
signal(SIGCHLD, handler);
pid = fork();
if (pid == 0)
{
while (1)
{
scanf("%c %s", &c.type, c.name);
getchar();
//登录
if (c.type == 'L' || c.type == 'l')
{
if (shu == 0)
{
//发送。
m = sendto(lfd, (char *)&c, 256, 0, (struct sockaddr *)&caddr, clen);
if (m == -1)
{
perror("recvfeom error");
}
shu++;
}
else
printf("请勿重复登录!\n");
}
//退出
else if (c.type == 'Q' || c.type == 'q')
{
if (shu > 0)
{
strncpy(c.text, "Q", 1);
m = sendto(lfd, (char *)&c, 256, 0, (struct sockaddr *)&caddr, clen);
if (m == -1)
{
perror("recvfeom error");
}
exit(0);
}
else
printf("请先登录!\n");
}
//发消息
else if (c.type == 'C' || c.type == 'c')
{
if (shu > 0)
{
fgets(c.text, 256, stdin);
if (c.text[strlen(c.text) - 1] == '\n')
c.text[strlen(c.text) - 1] = '\0';
m = sendto(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&caddr, clen);
if (m == -1)
{
perror("recvfeom error");
}
printf("发送完成!\n");
}
else
printf("请先登录!\n");
}
}
}
if (pid > 0)
{
while (1)
{
printf("读取服务器终端.......\n");
int n = recvfrom(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, &clen);
if (n < 0)
{
perror("recvfrom error");
return -1;
}
if (s.type == 'L' || s.type == 'l')
{
printf("name:%s\n", s.text);
}
else if (s.type == 'Q' || s.type == 'q')
{
printf("name:%s\n", s.text);
}
// else if (s.type == 'C' || s.type == 'c')
else
{
printf("port:%d ip:%s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
printf("name:%s %s\n", s.name, s.text);
}
}
}
return 0;
}
//信号捕捉函数, 用于回收子线程
void handler()
{
waitpid(-1, NULL, WNOHANG);
exit(getppid());
}
服务器代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/wait.h>
typedef struct node_t
{
char ip[50];
int port;
struct node_t *next;
} link_node_t, *link_list_t;
typedef struct msg_t
{
char type; //L C Q
char name[32]; //用户名
char text[128]; //消息正文
} MSG_t;
int main(int argc, char const *argv[])
{
int lfd, cfd, ret;
char buf[256], file[256], ip[50];
MSG_t c, s;
//创建套接字
lfd = socket(AF_INET, SOCK_DGRAM, 0);
if (lfd == -1)
perror("socket error");
else
printf("套接字创建成功!\n");
//初始化地址结构
struct sockaddr_in laddr, caddr;
laddr.sin_family = caddr.sin_family = AF_INET;
laddr.sin_port = htons(7598);
laddr.sin_addr.s_addr = inet_addr("172.20.10.12");
socklen_t llen, clen;
llen = sizeof(laddr);
clen = sizeof(caddr);
ret = bind(lfd, (struct sockaddr *)&laddr, llen);
if (ret == -1)
{
perror("bind error");
}
//创建一个单项链表
link_list_t p = (link_list_t)malloc(sizeof(link_node_t));
if (p == NULL)
{
perror("p malloc error");
}
p->port = -1;
p->next = NULL;
link_list_t q = p;
while (1)
{
q = p;
//读取
printf("读取信息中!\n");
int m = recvfrom(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&laddr, &llen);
if (m == -1)
{
perror("recvfeom error");
return -1;
}
else
{
printf("服务器收到消息!\n");
}
printf("%s,%d\n", inet_ntoa(laddr.sin_addr), ntohs(laddr.sin_port));
if (c.type == 'L' || c.type == 'l')
{
printf("%s正在登录.....\n", c.name);
link_list_t new = NULL; //创建新链表保存,发端的地址结构
new = (link_list_t)malloc(sizeof(link_node_t));
if (new == NULL)
{
perror("new malloc error");
}
new->port = ntohs(laddr.sin_port);
strcpy(new->ip, inet_ntoa(laddr.sin_addr));
new->next = NULL;
q = p;
//尾插,判断是不是只有头部
if (q->next == NULL)
{
q->next = new;
}
//尾插
else
{
q = q->next;
while (q->next != NULL)
{
q = q->next;
}
q->next = new;
}
//找到头部,开始轮循发送谁登录上来,
//这地方我设置的一定是尾插,只用个前面发送登录信息即可,
q = p;
q = q->next;
while (1)
{
if (q->next == NULL) //所以在这个位置,我检测到是最后一个的时候,直接跳出即可
break;
else
{
caddr.sin_port = htons(q->port);
caddr.sin_addr.s_addr = inet_addr(q->ip);
s.type = c.type;
strcpy(s.name, c.name);
strcpy(s.text, c.name);
strcat(s.text, " record!");
sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
}
q = q->next;
}
printf("%s登录完成\n", c.name);
}
else if (c.type == 'Q' || c.type == 'q')
{
printf("%s退出中......\n", c.name);
q = p;
while (1)
{
if (q->next->port == ntohs(laddr.sin_port)) //这个地方我先将推出的那个客户端的地址结构从链表上移除
{ //然后我向剩下的所有人发退出信息即可
link_list_t pnew = q->next;
q->next = pnew->next;
free(pnew);
break;
}
else
q = q->next;
}
q = p;
if (q->next == NULL)
{
printf("退出完成!\n");
continue;
}
else //轮循发送推出信息
q = p->next;
while (1)
{
if (q->next == NULL) //这个地方是判断是不是最后一个,最后一个的话,发送给他之后,直接退出就行了
{
caddr.sin_port = htons(q->port);
caddr.sin_addr.s_addr = inet_addr(q->ip);
s.type = c.type;
strcpy(s.text, c.name);
strcat(s.text, " quit!");
sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
break;
}
caddr.sin_port = htons(q->port);
caddr.sin_addr.s_addr = inet_addr(q->ip);
s.type = c.type;
strcpy(s.text, c.name);
strcat(s.text, " quit!");
sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
q = q->next;
}
printf("%s退出完成\n", c.name);
}
else if (c.type == 'C' || c.type == 'c')
{
printf("%s接收消息中......\n", c.name);
s = c;
printf("type:%c name:%s text:%s\n", s.type, s.name, s.text);
q = p;
q = q->next;
while (1)
{
if (q->port == ntohs(laddr.sin_port)) //一个道理,即不要给自己发送消息
{
if(q->next == NULL)
break;
q = q->next;
continue;
}
if (q->next == NULL) //判断是不是最后一个
{
caddr.sin_port = htons(q->port);
caddr.sin_addr.s_addr = inet_addr(q->ip);
s = c;
sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
break;
}
else //不然轮循发送
{
caddr.sin_port = htons(q->port);
caddr.sin_addr.s_addr = inet_addr(q->ip);
s = c;
sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
q = q->next;
}
}
printf("%s发送消息完成\n", c.name);
}
}
return 0;
}
这里第一次实现这个功能写的不是很好,经老师指正之后改进点如下:
1.登录没有必要写道循环里面,写道循环里面不仅麻烦,反人类还会使代码的时间复杂度提高是很没有必要的做法。正确的是将登录写在程序运行的开始,先登录了才能收发消息和退出。
2.退出信息和收发消息可以写到一块,客户不会说是刻意的打上名字再打出quit消息,所以再客户端收发消息的时候判断客户写的消息内容,如是“quit”则退出客户端,并且杀死所有进程即可。
3.要注意的一个点,由于只是一个小小小的程序,所以在客户端这边常用Crtl+c来杀死进程,但是这样的话,服务器会残留杀死的这个客户端的链表信息,导致浪费,所以这里加入了判断SIGNAL函数,用以判断Crtl+c发送的sigint信号,当捕捉到该信号了以后,就先向服务器发送退出消息了以后,再将客户端退出了即可。
修改完的代码如下:
客户端的:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#define POST 7598
void handler();
void quit();
typedef struct msg_t
{
char type; //L登录 C聊天 Q退出
char name[32]; //用户名
char text[128]; //消息正文
} MSG;
struct sockaddr_in caddr;
socklen_t clen;
MSG c, s;
int lfd, m;
int main(int argc, char const *argv[])
{
int cfd, ret;
int shu = 0;
//创建套接字
lfd = socket(AF_INET, SOCK_DGRAM, 0);
if (lfd == -1)
perror("socket error");
//初始化地址结构
caddr.sin_family = AF_INET;
caddr.sin_port = htons(POST);
caddr.sin_addr.s_addr = inet_addr("172.20.10.12");
clen = sizeof(caddr);
pid_t pid;
//登录
printf("请输入名称\n");
scanf("%s", c.name);
getchar();
//发送。
c.type = 'l';
m = sendto(lfd, (char *)&c, 256, 0, (struct sockaddr *)&caddr, clen);
if (m == -1)
{
perror("recvfeom error");
}
shu++;
//设置信号捕捉函数,回收子进程
signal(SIGCHLD, handler);
signal(SIGINT, quit);
pid = fork();
if (pid == 0)
{
while (1)
{
fgets(c.text, 256, stdin);
if (c.text[strlen(c.text) - 1] == '\n')
c.text[strlen(c.text) - 1] = '\0';
if (strncmp(c.text, "quit", 4) == 0)
{
c.type = 'q';
m = sendto(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&caddr, clen);
if (m == -1)
perror("recvfeom error");
break;
}
else
{
c.type = 'c';
m = sendto(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&caddr, clen);
if (m == -1)
perror("recvfeom error");
}
}
close(lfd);
exit(-1);
return 0;
}
if (pid > 0)
{
while (1)
{
printf("读取服务器终端.......\n");
int n = recvfrom(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, &clen);
if (n < 0)
{
perror("recvfrom error");
return -1;
}
if (s.type == 'L' || s.type == 'l')
{
printf("name:%s\n", s.text);
}
else if (s.type == 'Q' || s.type == 'q')
{
printf("name:%s\n", s.text);
}
// else if (s.type == 'C' || s.type == 'c')
else
{
printf("port:%d ip:%s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
printf("name:%s %s\n", s.name, s.text);
}
}
}
return 0;
}
//信号捕捉函数, 用于回收子线程
void handler()
{
waitpid(-1, NULL, WNOHANG);
exit(getppid());
}
void quit()
{
c.type = 'q';
m = sendto(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&caddr, clen);
if (m == -1)
perror("recvfeom error");
exit(-1);
}
服务器的代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/wait.h>
typedef struct node_t
{
char ip[50];
int port;
struct node_t *next;
} link_node_t, *link_list_t;
typedef struct msg_t
{
char type; //L C Q
char name[32]; //用户名
char text[128]; //消息正文
} MSG_t;
void *server_send(void *arg);
MSG_t c, s;
int main(int argc, char const *argv[])
{
int lfd, cfd, ret;
char buf[256], file[256], ip[50];
//创建套接字
lfd = socket(AF_INET, SOCK_DGRAM, 0);
if (lfd == -1)
perror("socket error");
else
printf("套接字创建成功!\n");
//初始化地址结构
struct sockaddr_in laddr, caddr;
laddr.sin_family = caddr.sin_family = AF_INET;
laddr.sin_port = htons(7598);
laddr.sin_addr.s_addr = inet_addr("172.20.10.12");
socklen_t llen, clen;
llen = sizeof(laddr);
clen = sizeof(caddr);
ret = bind(lfd, (struct sockaddr *)&laddr, llen);
if (ret == -1)
{
perror("bind error");
}
//创建一个单项链表
link_list_t p = (link_list_t)malloc(sizeof(link_node_t));
if (p == NULL)
{
perror("p malloc error");
}
p->port = -1;
p->next = NULL;
link_list_t q = p;
pthread_t tid;
ret = pthread_create(&tid, NULL, server_send, NULL);
if (ret != 0)
{
perror("pthread err");
return -1;
}
pthread_detach(tid);
while (1)
{
q = p;
//读取
printf("读取信息中!\n");
int m = recvfrom(lfd, (char *)&c, sizeof(c), 0, (struct sockaddr *)&laddr, &llen);
if (m == -1)
{
perror("recvfeom error");
return -1;
}
else
{
printf("服务器收到消息!\n");
}
printf("%s,%d\n", inet_ntoa(laddr.sin_addr), ntohs(laddr.sin_port));
if (c.type == 'L' || c.type == 'l')
{
printf("%s正在登录.....\n", c.name);
//找到头部,开始轮循发送谁登录上来,
//这地方我设置的一定是尾插,只用个前面发送登录信息即可,
q = p;
while (q->next != NULL)
{
q = q->next;
caddr.sin_port = htons(q->port);
caddr.sin_addr.s_addr = inet_addr(q->ip);
s.type = c.type;
strcpy(s.name, c.name);
strcpy(s.text, c.name);
strcat(s.text, " record!");
sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
}
//创建新链表保存,发端的地址结构 = (link_list_t)malloc(sizeof(link_node_t));
link_list_t new = (link_list_t)malloc(sizeof(link_node_t));
if (new == NULL)
{
perror("new malloc error");
}
new->port = ntohs(laddr.sin_port);
strcpy(new->ip, inet_ntoa(laddr.sin_addr));
new->next = NULL;
q->next = new;
printf("%s登录完成\n", c.name);
}
else if (c.type == 'Q' || c.type == 'q')
{
printf("%s退出中......\n", c.name);
q = p;
while (q->next != NULL)
{
q = q->next;
if(q->next == NULL)
{
break;
}
if (q->port == ntohs(laddr.sin_port)) //这个地方我先将推出的那个客户端的地址结构从链表上移除
{ //然后我向剩下的所有人发退出信息即可
q->port = q->next->port;
strcpy(q->ip,q->next->ip);
q->next = q->next->next;
free(q->next);
}
printf("*****\n");
caddr.sin_port = htons(q->port);
caddr.sin_addr.s_addr = inet_addr(q->ip);
s.type = c.type;
strcpy(s.text, c.name);
strcat(s.text, " quit!");
sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
}
printf("%s退出完成\n", c.name);
}
else if (c.type == 'C' || c.type == 'c')
{
printf("%s接收消息中......\n", c.name);
s = c;
printf("type:%c name:%s text:%s\n", s.type, s.name, s.text);
q = p;
while (q->next != NULL)
{
q = q->next;
if (q->port == ntohs(laddr.sin_port)) //一个道理,即不要给自己发送消息
{
if (q->next == NULL)
break;
continue;
}
else //不然轮循发送
{
caddr.sin_port = htons(q->port);
caddr.sin_addr.s_addr = inet_addr(q->ip);
sendto(lfd, (char *)&s, sizeof(s), 0, (struct sockaddr *)&caddr, clen);
}
}
printf("%s发送消息完成\n", c.name);
}
}
return 0;
}
void *server_send(void *arg)
{
while (1)
{
fgets(s.text, 256, stdin);
if (s.text[strlen(s.text) - 1] == '\n')
s.text[strlen(s.text) - 1] = '\0';
// printf("%s\n", s.text);
}
}