1. 流程图
![](https://i-blog.csdnimg.cn/blog_migrate/ea14a0fc3aa837e5301729f5aa286c22.png)
2. 框架
2.1 客户端
//UDP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#define N 128
//__FILE__: 获取文件名
//__func__:获取函数名
//__LINE__:获取行号
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s-%d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
char buf[N] = {0};
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转化为无符号32位整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据 "1568" --> 1568
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//执行登录操作
//客户端输入用户名并将操作码和用户名发送给服务器
//do_login();
//登录成功之后创建子进程实现一边发送数据一边接收数据
pid_t pid;
if((pid = fork()) == -1)
{
ERRLOG("fork error");
}
else if(pid > 0) //父进程负责发送数据
{
//do_send();
}
else //子进程负责接收数据
{
//do_recv();
}
return 0;
}
2.2 服务器
//UDP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#define N 128
//__FILE__: 获取文件名
//__func__:获取函数名
//__LINE__:获取行号
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s-%d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
char buf[N] = {0};
ssize_t bytes;
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转化为无符号32位整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据 "1568" --> 1568
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//服务器创建子进程实现一边发送系统数据一边接收数据
pid_t pid;
if((pid = fork()) == -1)
{
ERRLOG("fork error");
}
else if(pid > 0) //父进程负责发送系统信息
{
//do_send();
}
else //子进程负责接收数据
{
if(recvfrom() == -1)
{
ERRLOG("recvfrom error");
}
//login:1 chat:2 quit:3
switch()
{
case 1:
//do_login();
break;
case 2:
//do_chat();
break;
case 3:
//do_quit();
break;
}
}
return 0;
}
3. 功能实现
3.1 客户端
//UDP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
//__FILE__: 获取文件名
//__func__:获取函数名
//__LINE__:获取行号
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s-%d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
#define N 32
typedef struct{
int code; //login:1 chat:2 quit:3
char name[N];
char text[N];
}MSG;
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转化为无符号32位整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据 "1568" --> 1568
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
MSG msg;
memset(&msg, 0, sizeof(msg));
//执行登录操作
//客户端输入用户名并将操作码和用户名发送给服务器
msg.code = 1;
printf("请输入用户名:");
fgets(msg.name, N, stdin);
msg.name[strlen(msg.name) - 1] = '\0';
if(sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("sendto error");
}
//登录成功之后创建子进程实现一边发送数据一边接收数据
pid_t pid;
if((pid = fork()) == -1)
{
ERRLOG("fork error");
}
else if(pid > 0) //父进程负责发送数据
{
while(1)
{
fgets(msg.text, N, stdin);
msg.text[strlen(msg.text) - 1] = '\0';
//判断是群聊信息还是退出信息
//退出信息
if(strcmp(msg.text, "quit") == 0)
{
//设置操作码并发送给服务器
msg.code = 3;
if(sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("sendto error");
}
//让整个客户端退出
kill(pid, SIGKILL);
exit(0);
}
//群聊信息
else
{
//设置操作码并发送数据给服务器
msg.code = 2;
if(sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("sendto error");
}
}
}
}
else //子进程负责接收数据
{
char buf[128] = {0};
while(1)
{
if(recvfrom(sockfd, buf, 128, 0, NULL, NULL) == -1)
{
ERRLOG("recvfrom error");
}
printf("%s\n", buf);
}
}
return 0;
}
3.2 服务器
//UDP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
//__FILE__: 获取文件名
//__func__:获取函数名
//__LINE__:获取行号
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s-%s-%d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
#define N 32
typedef struct{
int code; //login:1 chat:2 quit:3
char name[N];
char text[N];
}MSG;
//
typedef struct node{
struct sockaddr_in addr; //数据域,保存每一个登录的用户的信息
struct node *next; //指针域,保存下一个结点的地址
}linklist;
linklist *LinklistCreate();
void do_login(int sockfd, MSG msg, linklist *head, struct sockaddr_in clientaddr);
void do_chat(int sockfd, MSG msg, linklist *head, struct sockaddr_in clientaddr);
void do_quit(int sockfd, MSG msg, linklist *head, struct sockaddr_in clientaddr);
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
char buf[N] = {0};
ssize_t bytes;
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
//inet_addr:将点分十进制ip地址转化为无符号32位整形数据
//htons:将主机字节序转化为网络字节序
//atoi:将数字型字符串转化为整形数据 "1568" --> 1568
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//服务器创建子进程实现一边发送系统数据一边接收数据
pid_t pid;
if((pid = fork()) == -1)
{
ERRLOG("fork error");
}
else if(pid > 0) //父进程负责发送系统信息
{
//将父进程当做是客户端直接给子进程发送群聊信息即可
MSG msg;
msg.code = 2;
strcpy(msg.name, "server");
while(1)
{
fgets(msg.text, N, stdin);
msg.text[strlen(msg.text) - 1] = '\0';
if(sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("sendto error");
}
}
}
else //子进程负责接收数据
{
MSG msg;
linklist *head = LinklistCreate();
while(1)
{
if(recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
ERRLOG("recvfrom error");
}
printf("[%s-%d]: code:%d, name:%s, text:%s\n", inet_ntoa(clientaddr.sin_addr),\
ntohs(clientaddr.sin_port), msg.code, msg.name, msg.text);
//根据接收数据的操作码做出相应的处理
//login:1 chat:2 quit:3
switch(msg.code)
{
case 1:
do_login(sockfd, msg, head, clientaddr);
break;
case 2:
do_chat(sockfd, msg, head, clientaddr);
break;
case 3:
do_quit(sockfd, msg, head, clientaddr);
break;
}
}
}
return 0;
}
linklist *LinklistCreate()
{
linklist *h = (linklist *)malloc(sizeof(linklist));
h->next = NULL;
return h;
}
void do_login(int sockfd, MSG msg, linklist *head, struct sockaddr_in clientaddr)
{
//将新用户登录的信息组包发送给其他在线的用户
//组数据
char buf[128] = {0};
sprintf(buf, ">>> %s 上线了 <<<", msg.name);
//将组好的数据发送给其他在线的用户
linklist *p = head;
while(p->next != NULL)
{
p = p->next;
if(sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&p->addr, sizeof(struct sockaddr_in)) == -1)
{
ERRLOG("sendto error");
}
}
//将新登录的用户的信息保存在链表中
linklist *temp = (linklist *)malloc(sizeof(linklist));
temp->addr = clientaddr;
temp->next = NULL;
temp->next = head->next;
head->next = temp;
return ;
}
void do_chat(int sockfd, MSG msg, linklist *head, struct sockaddr_in clientaddr)
{
//将群聊信息组包后发送给其他在线的用户
//组包
char buf[128] = {0};
sprintf(buf, "%s: %s", msg.name, msg.text);
//遍历链表发送数据
linklist *p = head;
while(p->next != NULL)
{
p = p->next;
//发送者不接受自己发送的数据
//memcmp:比较两个内存区域的内容是否一样
if(memcmp(&p->addr, &clientaddr, sizeof(clientaddr)) != 0)
{
if(sendto(sockfd, buf, 128, 0, (struct sockaddr *)&p->addr, sizeof(clientaddr)) == -1)
{
ERRLOG("sendto error");
}
}
}
return ;
}
void do_quit(int sockfd, MSG msg, linklist *head, struct sockaddr_in clientaddr)
{
//将用户下线的信息组包发送给其他在线用户
//组包
char buf[128] = {0};
sprintf(buf, "------ %s 下线了 ------", msg.name);
//遍历链表发送数据给客户端
linklist *p = head;
linklist *temp;
while(p->next != NULL)
{
//将退出用户的结点从链表中删除
if(memcmp(&p->next->addr, &clientaddr, sizeof(clientaddr)) == 0)
{
temp = p->next;
p->next = temp->next;
free(temp);
temp = NULL;
}
else
{
if(sendto(sockfd, buf, 128, 0, (struct sockaddr *)&p->next->addr, sizeof(clientaddr)) == -1)
{
ERRLOG("sendto error");
}
p = p->next;
}
}
return ;
}