(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服务器代码实现

c 复制代码
#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客户端代码实现

c 复制代码
#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. 代码实现
    服务端代码:
c 复制代码
#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;
}

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

相关推荐
muxue1781 小时前
centos 7 网络配置(2):ping命令出现问题
linux·网络·centos
newki2 小时前
学习笔记,Linux虚拟机中C/C++的编译相关流程步骤
c语言·c++
2301_817031653 小时前
C语言-- 深入理解指针(4)
c语言·开发语言·算法
山猪打不过家猪4 小时前
(六)RestAPI 毛子(外部导入打卡/游标分页/Refit/Http resilience/批量提交/Quartz后台任务/Hateoas Driven)
网络·缓存
爱学习的章鱼哥4 小时前
如何实现一个可视化的文字编辑器(C语言版)?
c语言·编辑器·文本编辑器·程序设计·easyx
weixin138233951794 小时前
EN18031测试,EN18031认证,EN18031报告解读
网络
JhonKI5 小时前
【Linux网络】构建与优化HTTP请求处理 - HttpRequest从理解到实现
linux·网络·http
盐烟5 小时前
C语言-函数练习1
c语言·开发语言·笔记
simple_whu5 小时前
Visual Studio C/C++编译器cl.exe的/source-charset与/execution-charset设置项
c语言·c++·visual studio
GOATLong5 小时前
网络基础概念
linux·运维·服务器·网络·arm开发·c++