IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> (第六章)基于UDP的服务器端客户端 -> 正文阅读

[网络协议](第六章)基于UDP的服务器端客户端

一、理解UDP

1.1 UDP套接字特点

? UDP工作原理就跟寄信一样,写上收信地址以及收信人然后贴上邮票就可以发送了,发送完之后就不管有没有发送到即不确认收信人是否收到信件。与TCP对比,少了可靠性而多了简洁性,UDP不会发送类似ACK这类应答信息,也不会像SEQ那样给数据包分配序列号。在注重效率而非准确性的地方,采用UDP通信是一个很好的选择。

? 为了提供可靠的数据传输服务TCP在不可靠的IP层进行流控制,而UDP就缺少这种流控制机制。流控制是区分UDP与TCP的重要标志,因为TCP生命在于流控制,在上面几章里面讲的“与对方套接字连接以及断开连接过程”也是属于流控制的一部分。

1.2 UDP内部工作原理

UDP最重要的作用就是根据端口号将 传到主机的数据包交给最终UDP套接字,仅此而已。是不是很简单?

二、实现基于UDP的服务器端/客户端

UDP不是面向连接的,因此不需要accept(),listen等函数,但需要绑定UDP套节字。在TCP当中,除了用于监听客户端连接请求的套接字外,其他套接字都是与客户端套接字一一对应的。比如服务端要向10个客户端服务,就需要除守门套接字之外还需10个套接字专门处理与客户端的数据通信。但是在UDP当中,不管是客户端还是服务端都只需要一个套接字就可以。

2.1 操作函数

#include<sys/socket.h>
//数据发送函数
ssize_t sendto(int sock,void *buff,size_t nbytes,int flags,struct sockaddr *to,socklen_t addrlen);
//成功返回传输的字节数,失败时返回-1;
//sock: UDP套接字
//buff:保存待传输数据的缓冲地址值
//nbytes:待传输的数据长度,以字节为单位
//flag:可选参数,一般写0
//to:存有目标地址信息的sockaddr结构体变量的地址值
//addrlen:传递给参数to的地址值结构体变量长度


//数据接收函数
ssize_t recvvfrom(int sock,void *buff,size_t nbytes,int flags,struct sockaddr *from,socklen_t addrlen)
//成功返回传输的字节数,失败时返回-1;
//sock: UDP套接字
//buff:保存接收数据的缓冲地址值
//nbytes:可接收的最大字节数,故无法超过参数buf所指的缓冲大小
//flag:可选参数,一般写0
//to:存有发送端的地址信息的sockaddr结构体变量的地址值
//addrlen:传递给参数to的地址值结构体变量长度

UDP通信核心就是上面两个函数。

接下来我们采用UDP编写回声服务器以及客户端

2.2 基于UDP的回声服务器服务端/客户端

由于没有accept以及listen(),因此此处不存在服务端以及客户端的区别,但是不妨碍我们这样命名,这点需要知道

//server.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc,char *argv[])
{
  int serv_sock;
  char message[BUF_SIZE];
  int str_len;
  socklen_t clnt_adr_sz;
  struct sockaddr_in serv_adr,clnt_adr;
  if(argc!=2)
  {
      printf("Usage: %s <port>\n",argv[0]);
      exit(1);
  }
  serv_sock=socket(PF_INET,SOCK_DGRAM,0);
  if(serv_sock==-1)
  {
      error_handling("UDP socket creation error");
  }
  memset(&serv_adr,0,sizeof(serv_adr));
  serv_adr.sin_family=AF_INET;
  serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
  serv_adr.sin_port=htons(atoi(argv[1]));


  if(bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
    error_handling("bind() error");
  while(1)
  {
      clnt_adr_sz=sizeof(clnt_adr);
      str_len=recvfrom(serv_sock,message,BUF_SIZE,0,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
      sendto(serv_sock,message,str_len,0,(struct sockaddr*)&clnt_adr,clnt_adr_sz);
  }
  close(serv_sock);
  return 0;
}
void error_handling(char *message)
{
   fputs(message,stderr);
   fputc('\n',stderr);
   exit(1);
}
//client.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc,char *argv[])
{
  int client_sock;
  char message[BUF_SIZE];
  int str_len;
  socklen_t clnt_adr_sz;

  
  struct sockaddr_in serv_adr,from_adr;
  if(argc!=3)
  {
      printf("Usage: %s <port>\n",argv[0]);
      exit(1);
  }
  client_sock=socket(PF_INET,SOCK_DGRAM,0);
  if(client_sock==-1)
  {
      error_handling("UDP socket creation error");
  }
  memset(&serv_adr,0,sizeof(serv_adr));
  serv_adr.sin_family=AF_INET;
  serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
  serv_adr.sin_port=htons(atoi(argv[2]));


  //if(bind(client_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)error_handling("bind() error");

  while(1)
  {
      fputs("Insert message(q to quit):",stdout);
      fgets(message,sizeof(message),stdin);
      if(!strcmp(message,"q\n")|| !strcmp(message,"Q\n"))break;
      sendto(client_sock,message,strlen(message),0,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
      str_len=recvfrom(client_sock,message,BUF_SIZE,0,(struct sockaddr*)&from_adr,&clnt_adr_sz);
     message[str_len]=0;
     printf("Message from server:%s",message);
  }
  close(client_sock);
  return 0;
}
void error_handling(char *message)
{
   fputs(message,stderr);
   fputc('\n',stderr);
   exit(1);
}

编译

gcc server.c -o server
gcc client.c -o client
-----------------------------------
./server 9091

./client 127.0.0.1 9091

上面是UDP实现的回声服务器、客户端实例,我们需要和之前写的TCP实现的实例作为对比。这里给出TCP服务端与客户端流程:

//TCP 服务端
socket()       //1. 创建套接字
|
bind()         //2. 分配套接字地址,或者叫做将套接字和地址绑定
|
listen()       //3. 等待连接请求
|
accept()       //4. 允许连接
|
read()/write() //5. 进行数据交换
|
close()        //6. 断开连接
-------------------------------------------------------------------------------------- 
//TCP客户端
socket()       //创建套接字
|
connect()      //请求连接
|
read()/write() //交换数据
|
close()        //关闭连接

在TCP客户端,我们知道,虽然我们没有手动将套接字与地址绑定(即分配IP与端口号),但是connect()函数自动帮我们完成,也就是说TCP客户端是需要将套接字与IP地址与端口号绑定的,但是在UDP客户端,我们并没有connect()函数,我们也没有手动分配IP地址与端口号,而且测试结果正常,客户端何时分配的IP地址与端口号呢?

2.3 UDP客户端套接字地址的分配

在传输数据(即调用sendto函数)之前应该完成对IP地址的绑定以及端口号;而这个工作我们一般使用bind()函数实现,而且bind()函数在UDP与TCP里面都能使用,并非TCP专有,采用bind()函数分配IP地址与端口号属于手动方式,而在UDP回声服务端\客户端中我们并没有使用bind()函数也能实现数据传输是因为在调用sendto()函数时如果发现尚未分配IP地址与端口号,则在首次调用sendto()函数时会给相应套接字自动分配IP和端口,而且分配地址一直保留到程序结束为止。

三、UDP的数据传输特性和调用connect函数

UDP的数据传输特性是指UDP数据传输存在数据边界(TCP数据传输不存在数据边界),并且我们将在这一节验证这一特性。我们将在在这节讨论UDP中connect函数的调用。

3.1 UDP存在数据边界验证

//host1.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc,char* argv[])
{

    int sock;
    char message[BUF_SIZE];
    struct sockaddr_in my_adr,your_adr;
    socklen_t adr_sz;
    int str_len,i;
    if(argc!=2)
    {
        printf("Usage:%s <port>\n",argv[0]);
        exit(1);
    }
    sock=socket(PF_INET,SOCK_DGRAM,0);
    if(sock==-1)error_handling("socket error");
    memset(&my_adr,0,sizeof(my_adr));
    my_adr.sin_family=AF_INET;
    my_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    my_adr.sin_port=htons(atoi(argv[1]));

    if(bind(sock,(struct sockaddr*)&my_adr,sizeof(my_adr))==-1)error_handling("bind error");
    //循环读取数据,验证一次不能全部读完
    for(int i=0;i<3;i++)
    {
       sleep(5);
       adr_sz=sizeof(your_adr);
       str_len=recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*)&your_adr,&adr_sz);
       printf("Message %d:%s\n",i+1,message);
    }
    close(sock);
    return 0;
}
void error_handling(char *message)
{
   fputs(message,stderr);
   fputc('\n',stderr);
   exit(1);
}
//host2.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc,char *argv[])
{
   int sock;
   char msg1[]="Hi!";
   char msg2[]="I'm another UDP host!";
   char msg3[]="Nice to meet you";
   struct sockaddr_in your_adr;
   socklen_t your_adr_sz;
   if(argc!=3)
   {
       printf("Usage:%s <port> \n",argv[0]);
       exit(1);
   }
   sock=socket(PF_INET,SOCK_DGRAM,0);
   if(sock==-1)error_handling("socket() error");
   memset(&your_adr,0,sizeof(your_adr));
   your_adr.sin_family=AF_INET;
   your_adr.sin_addr.s_addr=inet_addr(argv[1]);
   your_adr.sin_port=htons(atoi(argv[2]));
   //调用三次sendto函数
   sendto(sock,msg1,sizeof(msg1),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
   sendto(sock,msg2,sizeof(msg2),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
   sendto(sock,msg3,sizeof(msg3),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
   close(sock);
   return 0;
}
void error_handling(char *message)
{
   fputs(message,stderr);
   fputc('\n',stderr);
   exit(1);
}

host2.c调用三次sendto()函数传输数据,而host1.c调用三次接收数据。需要注意的是在host1.c我们设定了延时sleep(5),也就是每次接收数据间隔为5s,但是在host2.c当中我们并没有设置延时,也就是说在host2.c通过调用三次sendto()函数将数据发送给了host2,如果是TCP,这时只需要调用一次读取函数即可全部读取,UDP则不同,在这种情况下也需要调用3次recvfrom函数依次读取,运行结果如下 所示。

运行结果

//先执行host1.c
./host1 9091
//在执行host2.c
./host2 127.0.0.1 9091
---------------------------------------------
host1收到结果:
Message 1:Hi!
Message 2:I'm another UDP host!
Message 3:Nice to meet you
----------------------------------------------
说明确实调用了三次recvfrom函数用于接收数据

UDP数据报概念

UDP套接字传输的数据包又称为数据报,实际上数据报也是属于数据包的一种。与TCP不同的是其本身可以成为1个完整数据(由于缺少流控制机制)。UDP存在数据边界,一个数据包即可成为一个完整数据因此成为数据报。

3.2 已连接UDP套接字与未连接UDP套接字

TCP套接字中需要注册(绑定)待传数据的目标IP与端口号,而UDP不需要。因此通过sendto()函数传递数据的过程分为以下三步:

  1. 向UDP套接字自动分配目标IP和端口号(非与套接字绑定)
  2. 传输数据
  3. 删除UDP套接字中注册的目标地址信息。

每次调用sendto()函数,上述三个过程都会执行一遍,也就是说调用N次sendto()函数所使用的目标地址信息都不同,因此我们可以用同一套UDP套接字服务多个目标。这种未注册目标地址信息的套接字称为未连接套接字,反之注册了目标地址信息的套接字称为已连接套接字。默认情况下UDP属于未连接套接字,但假如我们使用UDP实现如下场景:

IP为211.210.147.82的主机87号端口号准备了三个数据,调用3次sendto()进行传输。

这种要在三次sendto()使用同一个目标地址信息就很难实现,因此我们需要连接的UDP套接字。

3.3 创建已连接UDP套接字

过程很简单,在TCP套接字当中,我们使用connect注册目标地址信息,而connect函数在UDP与TCP当中都能用,因此我们只需加一个connect()函数就实现了。

sock=socket(PF_INET,SOCK_DGRAM,0);
memset(&adr,0,sizeof(adr));
adr.sin_family=AF_INET;
adr.sin_addr.s_addr=inet_addr("211.210.147.82");
adr.sin_port=htons(atoi("87");
connect(sock,(struct sockaddr*)&adr,sizeof(adr));
//其他过程不变

注意,UDP使用connect函数注册目标地址信息并不意味着要与对方UDP套接字进行连接。

与未连接套接字不同的事,之后调用sendto()函数进行发送数据时不需要自动分配IP地址以及端口号,因为已经已指定了目标,此时sendto()变成纯粹的传输数据的函数,我们不仅仅能使用sendto()、recvfrom()进行收发数据,我们还能直接使用write()、read()进行通信。下面将2.2 节当中的client.c改写成write()、read()函数进行通信的方式。server.c不变。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc,char *argv[])
{
  int client_sock;
  char message[BUF_SIZE];
  int str_len;
  socklen_t clnt_adr_sz;

  
  struct sockaddr_in serv_adr,from_adr;  //此时不再需要from_adr
  if(argc!=3)
  {
      printf("Usage: %s <port>\n",argv[0]);
      exit(1);
  }
  client_sock=socket(PF_INET,SOCK_DGRAM,0);
  if(client_sock==-1)
  {
      error_handling("UDP socket creation error");
  }
  memset(&serv_adr,0,sizeof(serv_adr));
  serv_adr.sin_family=AF_INET;
  serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
  serv_adr.sin_port=htons(atoi(argv[2]));
  //注册目标地址信息
  connect(client_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
    
  while(1)
  {
      fputs("Insert message(q to quit):",stdout);
      fgets(message,sizeof(message),stdin);
      if(!strcmp(message,"q\n")|| !strcmp(message,"Q\n"))break;
      //sendto(client_sock,message,strlen(message),0,(struct sockaddr*)&serv_adr,sizeof(serv_adr));
      write(client_sock,message,strlen(message));
      //str_len=recvfrom(client_sock,message,BUF_SIZE,0,(struct sockaddr*)&from_adr,&clnt_adr_sz);
      str_len=read(client_sock,message,sizeof(message)-1);
      
     message[str_len]=0;
     printf("Message from server:%s",message);
  }
  close(client_sock);
  return 0;
}
void error_handling(char *message)
{
   fputs(message,stderr);
   fputc('\n',stderr);
   exit(1);
}
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-07-22 23:06:44  更:2021-07-22 23:07:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/7 5:34:45-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码