基本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. 移动端调试工具weinre安装教程(java版)

    先申明:本安装教程是基于java的jdk安装的,经过测试可以正常使用,基于nodejs的安装,小喵鼓弄了好几天也没有成功,如果哪位童鞋基于nodejs安装成功了,请联系小喵,小喵在这里先谢谢你了! 好 ...

  2. WePY为了兼容支付宝小程序,改了好几十行代码

    早在16年底,就有流出支付宝在做小程序的事情,见<如何看待支付宝推出「小程序」?>,今年8月18号支付宝版本小程序的终于公测,十月怀胎实属不易啊. 紧接着就有人给我提ISSUE了: 此时我 ...

  3. mysql find_in_set在oracle下的解决方案

    比如一张表: artile (id,type,content); type:1表示文艺类,2表示小说类,3表示传记,4表示传说,等等5,6,7,8 表数据: id type content 1 3,1 ...

  4. JavaScript实现简单轮播图动画

    运行效果: 源代码: <!DOCTYPE html> <html lang="zh"> <head> <meta charset=&quo ...

  5. Java实现单链表的合并(保证数据的有序性)

    一.思路 1.比较两个链表的大小 2.将小链表插入到大链表中 3.使用插入保证链表数据的有序性 二.核心代码 /** * 合并两个链表,并且按照有序合并 * @param singleLinkedLi ...

  6. DirectX11 With Windows SDK--38 级联阴影映射(CSM)

    前言 在31章我们曾经实现过阴影映射,但是受到阴影贴图精度的限制,只能在场景中相当有限的范围内投射阴影.本章我们将以微软提供的例子和博客作为切入点,学习如何解决阴影中出现的Atrifacts: 边缘闪 ...

  7. 自己写的一个Hash文件校验软件

    原因 学校网络安全课讲到了Hash函数,老师提了一句上机操作的时候可以用自己的写的文件校验软件,所以我干脆就自己写一个. 说明 支持算法 MD5 SHA1 SHA256 SHA512 SHA384 为 ...

  8. 删库到跑路?还得看这篇Redis数据库持久化与企业容灾备份恢复实战指南

    本章目录 0x00 数据持久化 1.RDB 方式 2.AOF 方式 如何抉择 RDB OR AOF? 0x01 备份容灾 一.备份 1.手动备份redis数据库 2.迁移Redis指定db-数据库 3 ...

  9. 阶段性总结linux(1)

    学习安装linux系统 [网络连接方式] 桥接 ,好比所有人都在25期教室,公用这个教室的局域网段 192.168.11.0~192.168.11.255 教室内有60个同学,插上了网线,所有人都是 ...

  10. k8s入门之Service(六)

    将一组pod公开为网络服务,通过service代理,可以实现负载均衡 一.ClusterIP 此方式只能在集群内访问 1.使用命令暴露已存在的pod (1)继续使用前面章节的案例,查看名称为nginx ...