UDP 客户端 和 服务器 通信
要求
有新用户登录,其他在线的用户可以收到登录信息
有用户群聊,其他在线的用户可以收到群聊信息
有用户退出,其他在线的用户可以收到退出信息
服务器可以发送系统信息
提示
客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程
服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程
服务器需要给多个用户发送数据,所以需要保存每一个用户的信息,使用链表来保存 数据传输的时候要定义结构体,结构体中包含操作码、用户名以及数据
代码实现
服务器—01server.c
//-------------------------服务器-------------------------
#include "./uDP.h"
typedef struct __MSG{
char code;//'L' 登录 'C' 群聊 'Q' 退出
char name[32];//用户名
char text[128];
}msg_t;
//链表的节点
typedef struct __NODE{
struct sockaddr_in client_addr;
struct __NODE *next;
}node_t;
//创建链表的节点
void create_node(node_t **phead);
//将新登录的用户的网路信息结构体插入链表:头插
void do_login(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd);
//群聊--遍历链表--群发
void do_chat(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd);
//退出--群发--删除
void do_quit(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd);
int main(int argc, char *argv[]){
if(3 != argc){
printf("Usage : %s <IP> <PORT>\n", argv[0]);
exit(-1);
}
//创建套接字
int sockfd = 0;
if(-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0))){
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t server_addr_len = sizeof(server_addr);
//将套接字与服务器的网络信息结构体绑定
if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, server_addr_len)){
ERRLOG("bind error");
}
//用户信息结构体
msg_t msg;
memset(&msg, 0, sizeof(msg_t));
pid_t pid = 0;
if(-1 == (pid = fork())){
ERRLOG("fork error");
}else if(0 == pid){//子进程逻辑
//接收数据
//定义一个结构体,用于保存客户端的信息
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));
socklen_t client_addr_len = sizeof(client_addr);
//创建保存客户端网络信息结构体链表的头结点
node_t *phead = NULL;
create_node(&phead);
while(1){
memset(&msg, 0, sizeof(msg_t));
if(-1 == recvfrom(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&client_addr, &client_addr_len)){
ERRLOG("recvfrom error");
}
printf("%s : %s\n", msg.name, msg.text);
switch(msg.code){
case 'L':
//将新登录的用户的网路信息结构体插入链表:头插
do_login(phead, client_addr, msg, sockfd);
break;
case 'C':
//群聊--遍历链表--群发
do_chat(phead, client_addr, msg, sockfd);
break;
case 'Q':
//退出--群发--删除
do_quit(phead, client_addr, msg, sockfd);
break;
}
}
//关闭套接字
close(sockfd);
}else{//父进程逻辑
//发送系统消息
//把父进程看成客户端 以群聊的方式将 系统消息发给子进程
while(1){
memset(&msg, 0, sizeof(msg_t));
msg.code = 'C';
strcpy(msg.name, "server");
fgets(msg.text, 128, stdin);
msg.text[strlen(msg.text)-1] = '\0';
if(-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&server_addr, server_addr_len)){
ERRLOG("recvfrom error");
}
}
//关闭套接字
close(sockfd);
}
return 0;
}
//创建链表的节点
void create_node(node_t **phead){
*phead = (node_t *)malloc(sizeof(node_t));
memset(*phead, 0, sizeof(node_t));
}
//登录
void do_login(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd){
//先遍历链表 将 "xxx 登录" 的消息发给所有人
node_t *ptemp = phead;
while(ptemp->next != NULL){
ptemp = ptemp->next;
if(-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr))){
ERRLOG("sendto error");
}
}
//将新登录的用户的网路信息结构体插入链表:头插
node_t *pnew = NULL;
create_node(&pnew);
pnew->client_addr = client_addr;
pnew->next = phead->next;
phead->next = pnew;
}
//群聊
void do_chat(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd){
//遍历链表,将消息转发给除了自己之外的所有人
node_t *ptemp = phead;
while(ptemp->next != NULL){
ptemp = ptemp->next;
//不是自己再转发,是自己就不用发了
if(memcmp(&(ptemp->client_addr), &client_addr, sizeof(struct sockaddr_in))){
if(-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr))){
ERRLOG("sendto error");
}
}
}
}
//退出
void do_quit(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd){
//遍历链表,不是自己就转发"xxx 退出"的消息
//是自己 就将自己在链表中删除
node_t *ptemp = phead;
node_t *pdel = NULL;
while(ptemp->next != NULL){
if(memcmp(&(ptemp->next->client_addr), &client_addr, sizeof(struct sockaddr_in))){
//不是自己就转发
ptemp = ptemp->next;//只有不删除节点的情况下 ptemp才往后走
if(-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr))){
ERRLOG("sendto error");
}
}else{
//是自己 就将自己在链表中删除
pdel = ptemp->next;
ptemp->next = pdel->next;
free(pdel);
pdel = NULL;
}
}
}
客户端—02client.c
//--------------------------------客户端------------------
#include "./uDP.h"
typedef struct __MSG
{
char code; //'L' 登录 'C' 群聊 'Q' 退出
char name[32]; //用户名
char text[128];
} msg_t;
int main(int argc, char *argv[])
{
if (3 != argc)
{
printf("Usage : %s <IP> <PORT>\n", argv[0]);
exit(-1);
}
//创建套接字
int sockfd = 0;
if (-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0)))
{
ERRLOG("socket error");
}
//填充服务器网络信息结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t server_addr_len = sizeof(server_addr);
//组装第一个包
msg_t msg;
memset(&msg, 0, sizeof(msg_t));
printf("请输入用户名:");
fgets(msg.name, 32, stdin);
msg.name[strlen(msg.name) - 1] = '\0';
msg.code = 'L'; //登录
strcpy(msg.text, "加入群聊");
//先将xxx 加入群聊的消息发给服务器
if (-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&server_addr, server_addr_len))
{
ERRLOG("sendto error");
}
pid_t pid = 0;
if (-1 == (pid = fork()))
{
ERRLOG("fork error");
}
else if (0 == pid)
{ //子进程逻辑
//接收数据
while (1)
{
if (-1 == recvfrom(sockfd, &msg, sizeof(msg_t), 0, NULL, NULL))
{
ERRLOG("recvfrom error");
}
printf("%s : %s\n", msg.name, msg.text);
}
}
else
{ //父进程逻辑
while (1)
{
//发送数据
fgets(msg.text, 128, stdin);
msg.text[strlen(msg.text) - 1] = '\0';
//需要根据输入内容的不同组装不同的包
if (0 == strcmp(msg.text, "quit"))
{ //表示输入的是退出的消息
msg.code = 'Q';
strcpy(msg.text, "退出群聊");
}
else
{
msg.code = 'C';
}
//将消息发给服务器
if (-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&server_addr, server_addr_len))
{
ERRLOG("sendto error");
}
//如果将退出的消息发给了服务器,那么客户端就可以break然后退出了
if (0 == strcmp(msg.text, "退出群聊"))
{
break;
}
}
//让子进程自杀
kill(pid, SIGKILL);
wait(NULL);
//关闭套接字
close(sockfd);
}
return 0;
}
执行结果
注意
- 链表只需插入第一次连接的(新登录的用户)网路信息结构体插入链表
//先遍历链表 将 "xxx 登录" 的消息发给所有人
node_t *ptemp = phead;
while (ptemp->next != NULL)
{
ptemp = ptemp->next;
if (-1 == sendto(sockfd, &msg, sizeof(msg_t), 0,
(struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr)))
{
ERRLOG("sendto error");
}
}
//将新登录的用户的网路信息结构体插入链表:头插
node_t *pnew = NULL;
create_node(&pnew);
pnew->client_addr = client_addr;
pnew->next = phead->next;
phead->next = pnew;
- 群聊时,将消息转发给除了自己之外的所有人
//遍历链表,将消息转发给除了自己之外的所有人
node_t *ptemp = phead;
while (ptemp->next != NULL)
{
ptemp = ptemp->next;
//不是自己再转发,是自己就不用发了
if (memcmp(&(ptemp->client_addr), &client_addr,
sizeof(struct sockaddr_in)))
{
if (-1 == sendto(sockfd, &msg, sizeof(msg_t),0,
(struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr)))
{
ERRLOG("sendto error");
}
}
}
- 退出时,判断ptemp的下一节点,若将自己从链表中删除则不需往后走
//遍历链表,不是自己就转发"xxx 退出"的消息
//是自己 就将自己在链表中删除
node_t *ptemp = phead;
node_t *pdel = NULL;
while (ptemp->next != NULL)
{
if (memcmp(&(ptemp->next->client_addr), &client_addr,
sizeof(struct sockaddr_in)))
{
//不是自己就转发
ptemp = ptemp->next; //只有不删除节点的情况下 ptemp才往后走
if (-1 == sendto(sockfd, &msg, sizeof(msg_t),0,
(struct sockaddr *)&(ptemp->client_addr),sizeof(ptemp->client_addr)))
{
ERRLOG("sendto error");
}
}
else
{
//是自己 就将自己在链表中删除
pdel = ptemp->next;
ptemp->next = pdel->next;
free(pdel);
pdel = NULL;
}
}
- 为实现父进程发送系统消息,将消息发给子进程
//把父进程看成客户端 以群聊的方式将 系统消息发给子进程
while (1)
{
memset(&msg, 0, sizeof(msg_t));
msg.code = 'C';
strcpy(msg.name, "server");
fgets(msg.text, 128, stdin);
msg.text[strlen(msg.text) - 1] = '\0';
if (-1 == sendto(sockfd, &msg, sizeof(msg_t), 0,
(struct sockaddr *)&server_addr, server_addr_len))
{
ERRLOG("recvfrom error");
}
}