基本UDP套接字编程

系列文章导航:《Unix 网络编程》笔记

UDP 概述

流程图

recvfrom 和 sendto

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
struct sockaddr * from, socklen_t * addrlen); ssize_t sendto(int sockfd, const void * buff, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t addrlen);

参数说明

  • 前三个参数等同于 read 和 write 的三个参数:描述符、指向读或写缓冲区的指针、读写字节数
  • sendto 的 to 参数指向一个含有数据报接收者的协议地址的套接字,其长度由 addrlen 指定
  • recvfrom 的 from 参数指向一个将由该函数在返回时填写数据报发送者的协议地址的套接字地址结构,注意长度是一个指针

注意

  • 长度为 0 的数据报是可以发送,也可以被接收到的
  • 如果 recvfrom 的 from 参数是一个空指针,那么相应的长度参数也必须是空指针,表示我们不关心发送者的协议地址
  • recvfrom 和 sendto 也可以用于 TCP,尽管通常没有理由这么做

程序代码

graph LR;

A[标准输入/输出] --fgets--> B[UDP-Client] --sendto/recvfrom--> C[UDP-Server]
C --recvfrom/sendto--> B --fputs--> A

服务器端

udpserv01.c

int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr, cliaddr; sockfd = Socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT); Bind(sockfd, (SA *)&servaddr, sizeof(servaddr)); dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr));
}

dg_echo.c

void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen) {
int n;
socklen_t len;
char mesg[MAXLINE]; for (;;) {
len = clilen;
n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &clilen);
Sendto(sockfd, mesg, n, 0, pcliaddr, len);
}
}

服务模型迭代服务器

graph LR;

subgraph 缓冲FIFO
数据报3
数据报2
数据报1
end

subgraph Client
CliSocket --发送一个数据报--> 数据报3
end

subgraph UDPServer
数据报1 --从缓冲中取出数据报--> ServSocket
end

因为没有连接的概念,无需维持状态,所以是来一个消费一个,有点像消费队列的感觉。

客户端

udpcli01.c

#include "unp.h"

int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr; if (argc != 2)
err_quit("usage: udpcli <IPaddress>"); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); sockfd = Socket(AF_INET, SOCK_DGRAM, 0); dg_cli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr)); exit(0);
}

dg_cli.c

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) {
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
while (Fgets(sendline, MAxLINE, fp) != NULL) {
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
n = Recvfrom(sockfd, recvline, MAXlINE, 0, NULL, NULL);
recvline[n] = 0;
Fputs(recvline, stdout);
}
}

对于客户端而言,什么时候指派一个套接字:

  • TCP 下,connect 调用时进行绑定
  • UDP 下,首次调用 sendto 时绑定

当前的问题

数据报的丢失

如果客户端发送的请求在去或回来的途中丢失了,那么客户端将会永远阻塞于 Recvfrom

简单的解决方法是为该方法设置一个超时,但是这种方法不能判断是发送的时候丢失了还是返回的途中丢失了,因此不具备可靠性。

接收到非目标的响应

问题

前文的代码中,由于如下语句:

n = Recvfrom(sockfd, recvline, MAXlINE, 0, NULL, NULL);

后两个参数为 NULL,则任何知道客户端临时端口的进程都能向客户发送数据报,与正常的服务器应答混淆。

解决方法

创建一个变量,接收发送方的协议地址,然后和我们期望的进行对比

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
socklen_t len;
struct sockaddr *preply_addr; preply_addr = Malloc(servlen); while (Fgets(sendline, MAXLINE, fp) != NULL)
{ Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); len = servlen;
n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0)
{
printf("reply from %s (ignored)\n",
Sock_ntop(preply_addr, len));
continue;
} recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}

仍然存在缺陷

在多宿服务器上,即一台主机有多个接口,也即有多个 IP 地址,有可能接收到的消息是从另一个接口发出的,从而被我们意外地拦截。

强端系统模型、弱端系统模型

解决方法:

  • 客户通过在 DNS 中查找服务器主机的名字来验证该主机的域名(而不是 IP 地址)
  • 给服务器的所有 IP 地址创建一个套接字,用 select 监听他们(后面有例子)

服务器未运行

从客户端的程序上来看,如果服务器没有运行,那么程序就会阻塞在 recvfrom 方法上。

通过 tcpdump 可以看到如下内容:

  • 确实有 ICMP 的错误信息表示该协议地址 UNREACHABLE,但是这个错误不会返回给客户端

  • 我们称这个 ICMP 错误为 异步错误 ,该错误由 sendto 引起,但是 sendto 本身却成功返回

  • 一个基本规则是,除非它已连接,否则其引发的异步错误并不返回给它

  • 后文将给出一个使用自己的守护进程获取未连接套接字上这些错误的简便方法

connect

简介

和 TCP 的 connect 相比:

UDP 调用 connect 的话:

  • 仍然不会有三次我偶在的过程
  • 内核只是检查是否存在立即可知的错误
  • 以及记录对端的 IP 地址和端口号

和没有 connect 的 UDP 相比:

  • 不能给输出操作指定目的 IP 和端口号了,即不使用 sendto 而是使用 write/send,写入内容自动发送到指定的协议地址

    其实可以用 sendto,但是不能指定目的地址,必须为空指针,第六个参数应该为 0(第五个参数为 NULL 时,第六个参数不再考虑)

  • 不必使用 recvfrom 获悉数据报的发送者,而改用 readrecvrecvmsg 。内核会过滤掉其他来源的数据报。

  • 由已连接 UDP 套接字引发的异步错误会返回给他们所在的进程

多次调用 connect

两个作用

  • 指定新的协议地址:和 TCP 的 connect 只能调用一次不同,UDP 可以多次调用以切换不同的协议地址
  • 断开套接字:将地址簇设为 AF_UNSPEC

性能

当要给同一目的地址发送多个数据报时,显式连接套接字效率更高

graph LR;

subgraph 建立连接
A(连接套接字) --> A1(发送1) --> A2(发送2) --> A3(断开连接)
end

subgraph 没有建立连接
B(连接套接字) --> B1(发送) --> B2(断开套接字) -.- B3(连接套接字) --> B4(发送) --> B5(断开套接字)
end

改写 dg_cli

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1]; Connect(sockfd, (SA *)pservaddr, servlen); while (Fgets(sendline, MAXLINE, fp) != NULL)
{ Write(sockfd, sendline, strlen(sendline)); n = Read(sockfd, recvline, MAXLINE); recvline[n] = 0; /* null terminate */
Fputs(recvline, stdout);
}
}

此时当我们建立连接时并不会报错,在我们用 Write 发送数据时会发现网络不可达的错误

流量控制

如果发送方的能力很强,且不断发数据报,而接收方处理的速度比较慢,则有可能将接收方的缓冲区冲垮

可以通过如下命令查看 UDP 数据的统计信息

netstat -s -p -u

可以通过如下方式增加缓冲区的大小:

n = 220 * 1024;
Setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

UDP 外出端口的确定

通过 connect 应用到 UDP 的一个副作用来获取:

int main(int argc, char **argv)
{
int sockfd;
socklen_t len;
struct sockaddr_in cliaddr, servaddr; if (argc != 2)
err_quit("usage: udpcli <IPaddress>"); sockfd = Socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Connect(sockfd, (SA *)&servaddr, sizeof(servaddr)); len = sizeof(cliaddr);
Getsockname(sockfd, (SA *)&cliaddr, &len);
printf("local address %s\n", Sock_ntop((SA *)&cliaddr, len)); exit(0);
}

select 结合 TCP 和 UDP

我们想做一个既能接收 TCP 请求,又能接收 UDP 请求的服务器。

思路是:把之前的并发 TCP 服务器和本章中的迭代 UDP 服务器结合成使用 select 来复用 TCP 和 UDP 套接字的程序。

具体来说,就是:

  • UDP 的描述符绑定 TCP 的端口
  • select 监听之前 TCP 提到的描述符,以及 UDP 的描述符
  • 在事件到达后判断是哪一种,然后触发相关的处理流程
int main(int argc, char **argv)
{
int listenfd, connfd, udpfd, nready, maxfdp1;
char mesg[MAXLINE];
pid_t childpid;
fd_set rset;
ssize_t n;
socklen_t len;
const int on = 1;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int); /* 4create listening TCP socket */
listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT); Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
Bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); /* 4create UDP socket */
udpfd = Socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT); Bind(udpfd, (SA *)&servaddr, sizeof(servaddr));
/* end udpservselect01 */ /* include udpservselect02 */
Signal(SIGCHLD, sig_chld); /* must call waitpid() */ FD_ZERO(&rset);
maxfdp1 = max(listenfd, udpfd) + 1;
for (;;)
{
printf("Hello UDP!\n");
FD_SET(listenfd, &rset);
FD_SET(udpfd, &rset);
if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0)
{
printf("NREADY, restart!\n");
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("select error\n");
} if (FD_ISSET(listenfd, &rset))
{
printf("listenfd selected!\n");
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *)&cliaddr, &len); if ((childpid = Fork()) == 0)
{ /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
} if (FD_ISSET(udpfd, &rset))
{
printf("udpfd selected!\n");
len = sizeof(cliaddr);
n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *)&cliaddr, &len); Sendto(udpfd, mesg, n, 0, (SA *)&cliaddr, len);
}
}
}
/* end udpservselect02 */

《Unix 网络编程》08:基本UDP套接字编程的更多相关文章

  1. 【Unix网络编程】chapter8基本UDP套接字编程

    chapter8基本UDP套接字编程 8.1 概述 典型的UDP客户端/服务端的函数调用 8.2 recvfrom和sendto函数 #include <sys/socket.h> ssi ...

  2. 【Python网络编程】利用Python进行TCP、UDP套接字编程

    之前实现了Java版本的TCP和UDP套接字编程的例子,于是决定结合Python的学习做一个Python版本的套接字编程实验. 流程如下: 1.一台客户机从其标准输入(键盘)读入一行字符,并通过其套接 ...

  3. 探索UDP套接字编程

    UDP和TCP处于同一层网络模型中,也就是运输层,基于二者之上的应用有很多,常见的基于TCP的有HTTP.Telnet等,基于UDP有DNS.NFS.SNMP等.UDP是无连接,不可靠的数据协议服务, ...

  4. 【转】 探索UDP套接字编程

    UDP和TCP处于同一层网络模型中,也就是运输层,基于二者之上的应用有很多,常见的基于TCP的有HTTP.Telnet等,基于UDP有DNS.NFS.SNMP等.UDP是无连接,不可靠的数据协议服务, ...

  5. JavaTCP和UDP套接字编程

    在我们刚开始入门Java后端的时候可能你会觉得有点复杂,包含了很多杂七杂八的知识,例如文件上传下载,监听器,JDBC,请求重定向,请求转发等等(当然也没有很多),但是我们自己真正的去开发一个小型网站( ...

  6. TCP和UDP套接字编程 (java实现)

    在了解网络编程之前,我们先了解一下什么叫套接字 套接字即指同一台主机内应用层和运输层之间的接口 由于这个套接字是建立在网络上建立网络应用的可编程接口 因此也将套接字称为应用程序和网络之间的应用程序编程 ...

  7. 计算机网络实验 UDP套接字编程

    这是个傻瓜式操作教程 西科大计算机网络实验 UDP套接字编程 我用自己的Ubuntu16.04来举例,实验室的是虚拟机,差不多 只针对第三个题目,修改服务器来通过响应客户端发送的GetTime并发送给 ...

  8. UDP套接字编程 返回系统时间

    计算机网络实验 简单UDP套接字编程 这是学校老师自己改进了一点的题目.我预习了好久才搞明白,同学来问的时候,一大堆简单问题实在是不想回答...所以,这时候我觉得博客是个好东西! 我的任务是做客户端和 ...

  9. <unix网络编程>UDP套接字编程

    典型的UDP客户/服务器程序的函数调用如下: 1.缓冲区 发送缓冲区用虚线表示,任何UDP套接字都有发送缓冲区,不过该缓冲区仅能表示写到该套接字的UDP数据报的上限.如果应用进程写一个大于套接字缓冲区 ...

随机推荐

  1. ES6-11学习笔记--模块化

    模块化规范有: CommonJS:Node.js AMD:require.js CMD:sea.js ES6:Module     ES6模块化使用: 关键词:export.import.as.exp ...

  2. python-使用函数求余弦函数的近似值

    本题要求实现一个函数,用下列公式求cos(x)近似值,精确到最后一项的绝对值小于eps(绝对值小于eps的项不要加): cos(x)=0!x0​−2!x2​+4!x4​−6!x6​+... 函数接口定 ...

  3. java中 什么叫隐藏(Hide)? 最好给个例子

    4.隐藏   技术核心和实例前面已经给出,这里只是给出大家对这种现象的一个定义而已,马克-to-win:子类重新定义一个与父类那里继承来的域变量完全相同的变量,称为域的隐藏.这里所谓隐藏是指子类拥有了 ...

  4. [ Linux ] 设置服务器开机自启端口

    https://www.cnblogs.com/yeungchie/ 需要用到的工具: crontab iptables crontab.set SHELL=/bin/bash PATH=/sbin: ...

  5. 浅谈一下流式处理平台Flink

    浅谈一下流式处理平台(Flink) 大数据框架听过很多,比如 Hadoop,HDFS...不过自己的项目都没有上过 为什么突然提到 Flink,因为最近一个项目需要用到,所以学习最好的方式就是项目驱动 ...

  6. 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客?

    前言 OK,我也来造轮子了 博客系统从一开始用WordPress,再到后来用hexo.hugo之类的静态博客生成放github托管,一直在折腾 折腾是为了更好解决问题,最终还是打算自己花时间搞一个好了 ...

  7. Elasticsearch8.1-ElasticsearchClient-Java客户端简单增删查改-随笔

    环境准备 Springboot 基本环境 自行前往https://start.spring.io/ 构建一个即可 Elasticsearch服务端 简单说下windows版本的安装  https:// ...

  8. What is ACPI

    What is ACPI, OnNow, and PCI Power Management? Microsoft began an initiative called OnNow to shorten ...

  9. findmnt、lsblk、mount 命令查看磁盘、目录挂载、挂载点以及文件系统格式等情况

    findmnt 展示出了目标挂载点( TARGET ).源设备( SOURCE ).文件系统类型( FSTYPE )以及相关的挂载选项( OPTIONS ),例如文件系统是否是可读可写或者只读的.根( ...

  10. @JsonFormat、@DateTimeFormat、@JsonSerialize注解的使用

    @JsonFormat 是jackson的注解,用于后台返回前台的时候将后台的date类型数据转为string类型格式化显示在前台,加在get方法或者date属性上面,因为 @JsonFormat 注 ...