当前位置: 首页 > article >正文

(c语言)网络编程之UDP认识与并发服务器实现

一、概述

UDP(User Datagram Protocol) : 用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可用于进行高效率的传输。 但不保证数据的可靠性

特点
UDP 是无连接的协议
UDP 不保证数据可靠性
UDP 是面向报文的
UDP通信的实时性较高

缺点:没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题。

使用场景
适合于广播/组播式通信中。
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输

在这里插入图片描述

二、UDP协议报文

用户数据报由两个部分组成:首部 + 数据部分。首部部分很简单,只有 8 个字节,由四个字段组成,每个字段的长度都是两个字节。

在这里插入图片描述
源端口号: 源端口号,需要对方回信时选用,不需要时全部置0.
目的端口号:目的端口号,在终点交付报文的时候需要用到。
长度:UDP的数据报的长度(包括首部和数据)其最小值为8(只有首部)
校验和:检测UDP数据报在传输中是否有错,有错则丢弃

UDP和TCP协议对比
特点
tcp协议是面向连接、可靠、基于字节流
udp协议是无连接、不可靠、基于数据报文

性能
tcp协议传输效率慢,所需要资源多
udp协议传输效率快,所需要资源少

应用常用
tcp协议常用于文件,邮件传输
udp协议常用于语音,视频,直播等实时性要求较高的场所

三、C语言中UDP实现

常见服务器类型:
处理多客户端的通讯处理方式不同,就导致服务器分为以下几种类型

  1. 迭代服务器
    大多数UDP都是迭代运行,服务器等待客户端的数据,收到数据后处理该数据,送回其应答,在等待下一个客户端请求。

在这里插入图片描述

  1. 并发服务器
    并发服务器是指在同一个时刻可以响应多个客户端的请求
    本质是创建 多进程/多线程 ,对多数用户的信息进行处理
    UDP协议一般默认是不支持多线程并发的 ,因为默认UDP服务器只有一个sockfd,所有的客户端都是通过同一个sockfd进行通信的。udp使用一个socket,如何做到做并发呢?(子进程/子线程中重新创建

(一)、UDP并发服务器之多进程并发

  1. 场景设计
    多个udp客户端需要先验证密钥是否正确后,才允许进行数据交互。假设密钥为"root"。(类似于登录功能)
    服务器接收到客户端信息,需要考虑两种情况
    <1>A用户的密钥验证请求消息
    <2>B用户的数据交互接收消息

  2. 框架图
    在这里插入图片描述

  3. 使用场景
    当UDP服务器与客户端交互多个数据报。问题在于每个客户都是往服务器端的同一个的端口发送数据,并用的同一个sockfd。并发服务器的每一个子进程如何正确区分每一个客户的数据报(涉及到进程的调度问题,如何避免一个子进程读取到不该它服务的客户发送来的数据报)。
    解决的方法是服务器(知名端口)等待客户的到来,当一个客户到来后,记下其IP和port,然后服务器fork一个子进程,建立一个socket再bind一个随机端口,然后建立与客户端的连接,并处理该客户的请求。父进程继续循环,等待下一个客户的到来。 在tftpd中就是使用这种技术的 。

代码实现

UDP服务器代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#define LOGIN_KEY "root"
#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0
int init_socket(const char* ip,const char* port)
{
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
 {
perror("socket");
return -1;
 }
// 绑定IP + 端口
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(port));
inet_aton(ip,&(addr.sin_addr));
int addrlen = sizeof(struct sockaddr_in);
if(bind(sockfd,(struct sockaddr*)&addr,addrlen)==-1)
 {
fprintf(stderr,"bind failed\n");
return -1;
 }
return sockfd;
}
int authentication_key(const char* ip,const char* port)
{
int sockfd = init_socket(ip,port);
if(sockfd == -1)
 {
return -1;
 }
char buf[512]={0};
int len = sizeof(buf);
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int new_sockfd;
// 循环验证用户密钥
while(1)
{
memset(buf,0,len);
ssize_t recvbytes = recvfrom(sockfd,buf,len,0,(struct
sockaddr*)&addr,&addrlen);
if(recvbytes == -1)
 {
perror("redvfrom");
return -1;
 }
unsigned char loginstatus = (strncmp(buf,LOGIN_KEY,4)==0)?
LOGIN_SUCCESS:LOGIN_FAILURE;
if(loginstatus == LOGIN_SUCCESS)
 {
pid_t pid = fork();
if(pid == -1)
 {
perror("fork");
return -1;
 }
else if(pid == 0)
 {
// 执行的是子进程
close(sockfd);//密钥验证成功,不需要sockfd文件描述符
new_sockfd = init_socket(ip,"0");// 绑定0端口,系统会随机分配一个可用的端口号
sendto(new_sockfd,&loginstatus,sizeof(loginstatus),0,(struct
sockaddr*)&addr,addrlen);
break;
 }
 }
else
 {
// 登录失败,使用原端口回复信息
ssize_t ret =
sendto(sockfd,&loginstatus,sizeof(loginstatus),0,(struct sockaddr*)&addr,addrlen);
 }
 }
return new_sockfd;
}
// 接收数据
void recv_data(int sockfd)
{
char buf[512]={0};
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
while(1)
 {
memset(buf,0,sizeof(buf));
ssize_t recvbytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct
sockaddr*)&client_addr,&addrlen);
if(recvbytes== -1)
 {
perror("recvfrom");
exit(EXIT_FAILURE);
 }
printf("client ip:%s\n",inet_ntoa(client_addr.sin_addr));
printf("client port:%d\n",ntohs(client_addr.sin_port));
printf("client content:%s\n",buf);
if(strncmp(buf,"quit",4)==0)
 {
break;
 }
 }
close(sockfd);
exit(EXIT_SUCCESS);
}
void signal_handler(int signum)
{
// 回收子进程的资源
waitpid(-1,NULL,WNOHANG);
printf("%s\n",strsignal(signum));
}
int main(int argc,char* argv[])
{
if(argc !=3)
 {
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);
 }
// 回收僵尸态的子进程[子进程结束后,会发SIGCHLD信号]
if(signal(SIGCHLD,signal_handler)==SIG_ERR)
 {
perror("signal error.");
exit(EXIT_FAILURE);
 }
// 验证秘钥
int sockfd = authentication_key(argv[1],argv[2]);
// recv data
recv_data(sockfd);
return 0;
}

UDP客户端代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0
// 发送交互消息
void send_message(int sockfd,struct sockaddr_in* addr,int addrlen)
{
char buf[512]={0};
while(1)
 {
memset(buf,0,sizeof(buf));
printf("client:");
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1]='\0';
int ret = sendto(sockfd,buf,strlen(buf),0,(struct
sockaddr*)addr,addrlen);
if(ret == -1)
 {
perror("client sendto");
exit(EXIT_FAILURE);
 }
if(strncmp(buf,"quit",4)==0)
 {
break;
 }
 }
}
void send_authentication_key(int sockfd,struct sockaddr_in* addr,struct
sockaddr_in* server_data_addr,int addrlen)
{
char buf[512]={0};
unsigned char flag = LOGIN_FAILURE;
while(1)
 {
putchar('>');
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin); //接收用户的输入
buf[strlen(buf)-1]='\0';
int ret = sendto(sockfd,buf,strlen(buf),0,(struct
sockaddr*)addr,addrlen);
if(ret == -1)
 {
perror("client sendto");
exit(EXIT_FAILURE);
}
// 接收服务器返回的消息
ssize_t recvbytes = recvfrom(sockfd,&flag,sizeof(flag),0,(struct
sockaddr*)server_data_addr,&addrlen);
if(recvbytes == -1)
 {
perror("client recvfrom");
exit(EXIT_FAILURE);
 }
if(flag == LOGIN_SUCCESS)
 {
break;
 }
 }
}
int main(int argc,char* argv[])
{
if(argc !=3)
 {
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);
 }
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
 {
perror("socket");
return -1;
 }
// 服务器端的地址
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int addrlen = sizeof(struct sockaddr_in);
// 处理数据的服务端信息
struct sockaddr_in server_data_addr;
memset(&server_data_addr,0,sizeof(struct sockaddr_in));
server_data_addr.sin_family =AF_INET;
// 发送密钥信息
send_authentication_key(sockfd,&addr,&server_data_addr,addrlen);
// 发送交互消息
send_message(sockfd,&server_data_addr,addrlen);
//关闭文件描述符
close(sockfd);
return 0;
}

在这里插入图片描述

(二)、UDP并发服务器之多线程并发

多线程并发服务器和多进程并发服务器的思路大同小异,都是服务器端等待客户端,当一个客户端到来后,记录其IP和port,然后同理,创建子线程,建立一个socket再bind一个随机端口,然后建立与客户端的连接,并处理该客户端的请求。父线程继续循环,等待下一个客户端的到来。 一个进程是可以绑定多个端口。

  1. 框架图
    在这里插入图片描述
  2. 代码实现
    服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define LOGIN_KEY "root"
#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0
typedef struct{
char ip[256];// ip
unsigned char flag; // 登录标识
struct sockaddr_in addr;// 数据交互的地址
}packet_t;
int init_socket(const char* ip,const char* port)
{
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
 {
perror("socket");
return -1;
 }
// 绑定IP + 端口
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(port));
inet_aton(ip,&(addr.sin_addr));
int addrlen = sizeof(struct sockaddr_in);
if(bind(sockfd,(struct sockaddr*)&addr,addrlen)==-1)
 {
fprintf(stderr,"bind failed\n");
return -1;
 }
return sockfd;
}
void* message_handle(void* argv)
{
void recv_data(int sockfd);
// 接收参数
packet_t* packet = (packet_t*)argv;
//创建新的socket
int new_sockfd = init_socket(packet->ip,"0");
if(new_sockfd == -1)
 {
perror("init_socket");
return NULL;
 }
// 给客户端响应登录成功
sendto(new_sockfd,&(packet->flag),sizeof(packet->flag),0,(struct
sockaddr*)&(packet->addr),sizeof(struct sockaddr));
// 数据交互
recv_data(new_sockfd);
pthread_exit(NULL);
}
void authentication_key(const char* ip,const char* port)
{
int sockfd = init_socket(ip,port);
if(sockfd == -1)
 {
return -1;
 }
char buf[512]={0};
int len = sizeof(buf);
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int new_sockfd;
pthread_t pid;
// 循环验证用户密钥
while(1)
 {
memset(buf,0,len);
ssize_t recvbytes = recvfrom(sockfd,buf,len,0,(struct
sockaddr*)&addr,&addrlen);
if(recvbytes == -1)
 {
perror("redvfrom");
return -1;
 }
unsigned char loginstatus = (strncmp(buf,LOGIN_KEY,4)==0)?
LOGIN_SUCCESS:LOGIN_FAILURE;
if(loginstatus == LOGIN_SUCCESS)
 {
// 创建子线程
packet_t packet;
strcpy(packet.ip,ip);
packet.flag = loginstatus;
packet.addr = addr;
pthread_create(&pid,NULL,message_handle,&packet);
// 线程分离
pthread_detach(pid);
 }
else
 {
// 登录失败,使用原端口回复信息
ssize_t ret =
sendto(sockfd,&loginstatus,sizeof(loginstatus),0,(struct sockaddr*)&addr,addrlen);
 }
 }
}
// 接收数据
void recv_data(int sockfd)
{
char buf[512]={0};
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
while(1)
 {
memset(buf,0,sizeof(buf));
ssize_t recvbytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct
sockaddr*)&client_addr,&addrlen);
if(recvbytes== -1)
 {
perror("recvfrom");
exit(EXIT_FAILURE);
 }
printf("client ip:%s\n",inet_ntoa(client_addr.sin_addr));
printf("client port:%d\n",ntohs(client_addr.sin_port));
printf("client content:%s\n",buf);
if(strncmp(buf,"quit",4)==0)
 {
break;
 }
 }
close(sockfd);
}
int main(int argc,char* argv[])
{
if(argc !=3)
 {
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);
 }
// 验证秘钥
authentication_key(argv[1],argv[2]);
return 0;
}

客户端代码与进程那块一样

在这里插入图片描述


http://www.kler.cn/a/596596.html

相关文章:

  • 40岁重启人生学Python,用煎饼摊理解函数,用快递盒认识变量
  • python3使用lxml解析xml时踩坑记录
  • SOFABoot-08-启动加速
  • thymelef
  • 《Python实战进阶》第43集:使用 asyncio 实现异步编程
  • windows下利用Ollama + AnythingLLM + DeepSeek 本地部署私有智能问答知识库
  • Unity/C# 常用XML读写方式详解(LINQ to XML、XmlReader/Writer)
  • ES集群的部署
  • Pytest的夹具
  • 论文阅读笔记——EWA Volume Splatting
  • CityEngine:3D城市建模专家
  • C++进阶——封装红黑树实现map和set
  • Selenium Web UI自动化测试:从入门到实战
  • C#与西门子PLC的六大通信库
  • 使用LangChain开发智能问答系统
  • 最优编码树的双子性
  • TopK问题
  • 常考计算机操作系统面试习题(二)(上)
  • AI生成移动端贪吃蛇游戏页面,手机浏览器打开即可玩
  • Linux进程控制(四)之进程程序替换