UDP套接字编程

目录

[1. 基础知识点](#1. 基础知识点)

[2. 网络字节序列](#2. 网络字节序列)

[3. socket编程接口](#3. socket编程接口)

[3.1 socket](#3.1 socket)

[3.2 bind](#3.2 bind)

[3.3 recvfrom 接收消息](#3.3 recvfrom 接收消息)

[3.4 sendto 发送消息](#3.4 sendto 发送消息)

[4. 简单的应用](#4. 简单的应用)


1. 基础知识点

我们所有的网络通信的行为,本质都是应用层进程间通信!只不过之前我们学习的是通过管道通信,现在是通过网络通信。

对双方而言,1.先把数据能到达自己的机器(ip地址) 2.找到指定的进程(port:端口号)。ip地址用来表示互联网中唯一的一台主机。端口号用来表示该指定的机器中进程的唯一性。{ip,port}可以表示互联网中唯一的一个进程,称为套接字,socket。

如何理解 port,端口号是一个16位数字,标识当前主机上唯一的网络进程。网络进程回合port进行绑定关联。为什么不用进程pid,它也可以标识一个进程,但因为PID是动态变化 的,所以网络协议规定:每个提供网络服务的软件,必须提前绑定一个固定的端口号。 每一个进程都有pid但不一定都有端口号**。**所以使用 socket 的好处是:

  1. 让其他模块(进程管理)和网络进行解耦。
  2. port专门用来进行网络通信。

一个端口号与一个进程相关联,一个端口号不可以和多个进程关联,一个进程可以和多个端口号关联,然后操作系统可以通过 hash 快速地通过端口号找到对应的进程。

认识TCP协议:此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

认识UDP协议:此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

这可靠和不可靠都是中性词。"可靠"就是"发出去的数据,对方百分之百确定收到了,且顺序完全正确";"不可靠"就是"尽力发,但到不到、到得全不全、乱没乱序,我不负责"。 TCP为了保证数据万无一失,内置了一套极其严苛的**"确认-重传"机制** 。UDP不提供 TCP那一套任何保证,错误也不会非常多,适合对数据可以可靠性要求不高的场景。事实上UDP反而用的多,TCP这么靠谱,干嘛还要UDP?这是因为**"可靠"是有巨大代价的** ,这个代价就是时间(延迟)资源(性能),要做更多的工作,更复杂

2. 网络字节序列

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  • TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节
  • 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
  • 如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略,直接发送即可。

举个例子:比如说数据是4字节的 0x aa bb cc dd,大端存储内存中就是aa bb cc dd,低地址就是aa,需要先发送 aa ,小端存储就是 dd cc bb aa,需要先发送 dd。但 TCP/IP 协议规定 我们要以 aa bb cc dd 的方式发送,即先发送 aa,大端可读性比较好。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

h:host主机,n:net网络,l:long32位,s:short 16位。

cpp 复制代码
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

3. socket编程接口

网络编程的时候,socket是有很多类别的,我们重点放在第二点。

  1. unix socket:域间socket ->同一台机器上文件路径,命名管道,本机内部进行通信。
  2. 网络socket:ip+port,网络通信也可以本地通信。
  3. 原始socket:应用层不使用 TCP/UDP,直接绕过传输层 ,访问网络层和数据链路层。通常用来编写一些网络工具

网络编程的时候又有不同的应用场景,理论上而言,我们应该给每种场景都设计一套编程接口,但设计者想用同一套接口完成,所以提出了struct sockaddr类型

大部分人网络编程接口要求传入 struct sockaddr* 类型了,内部可以通过判断struct sockaddr 的前两个字节,然后强转为struct sockaddr_in 或 struct sockaddr_un,也就 struct sockaddr 是基类。

为什么不使用 void*,因为当设计这些接口时,C语言不支持 void*。

3.1 socket

创建 socket 文件描述符,指向套接字对象 (TCP/UDP, 客户端 + 服务器),成功返回一个文件描述符,错误返回-1,错误吗被设置。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain 标识设置成哪种通信,AF_UNIX 域间通信,AF_INET 网络通信 是一个宏,是2。

type 标识套接字类型,一般经常使用的两种,SOCK_STREAM 流式套接, 通常时TCP 套接字,SOCK_DGRAM 面向数据报,无连接UDP使用。SOCK_RAW 底层原始套接。第三个参数表示你是使用TCP协议还是UDP协议,在网络通信中,前两个参数固定后,第三个参数缺省为0即可。

3.2 bind

绑定端口号 (TCP/UDP, 服务器),第一个参数是 socket 返回的文件描述符,第二个实际传入的是struct sockaddr_in,也是就是网络具体信息,内容如下图所示,第三个参数表示传入结构体的长度。绑定成功返回0,失败返回1。

cpp 复制代码
#include <netinet/in.h>
#include <arpa/inet.h>
int bind(int socket, const struct sockaddr *address,
            socklen_t address_len);

sockaddr_in 有一个成员是 sin_family ,这里看不出来因为使用了**##**可以把位于它两侧的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符:例如

cpp 复制代码
#define Add_TO_SUM(num, value)
    sum##num += value;
...
ADD_TO_SUM(5, 10);

最后变成 sum5 += 10。这样的连接必须产生要合成合法的标识符,否则其结果就是未定义的。

所以 struct sockaddr_in 中有 sin_family 这个成员,传入sin_,然后拼接 family,类型是 sa_family_t ,也就是无符号短整型,作用是指定这个地址结构使用的是哪种地址族, 对于 IPv4 来说,它必须被设成 AF_INET。因为套接字 API 用的是通用地址结构 struct sockaddr ,它本身不知道具体是 IPv4、IPv6 还是 Unix 域。所以每个具体结构(如 sockaddr_in)的第一个字段都用来打标签,告诉内核实际类型。

通过上面看struct in_addr 的定义,发现是一个uint32_t 类型。inet_addr 可以将字符串类型的 IP 地址 转化为 4 字节 IP 地址,并且变成网络序列。inet_ntoa 将4字节网络序列的 IP 地址转化为字符串。

cpp 复制代码
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);

char* inet_ntoa(struct in_addr in);

3.3 recvfrom 接收消息

cpp 复制代码
ssize_t recvfrom(int sockfd,void *buf,size_t len, int flags,
            struct sockaddr* src_addr, socklen_t* addrlen);

返回值是实际收到多少字节,buf 指向用户缓冲区,len是期望收到多少字节,sockfd 就是之前创建的套接字,从哪个套接字的接收队列里取数据。flags表示收数据的模式,0指阻塞式收消息。

后两个是输入输出型参数,src_addr 表示这条数据是谁发来的,addrlen 调用前表示:告诉内核 src_addr 缓冲区有多大,调用后:内核写回实际地址结构的长度,因为的第五个参数声明为 struct sockaddr*,它不知道你实际传入的是哪种地址结构,可能是struct sockaddr_in 也可能是 struct sockaddr_un。所以需要 addrlen 来传递长度信息。是为了让通用代码 能处理多种地址类型检测截断 、以及精确地回发数据。如果世界上只有 IPv4 一种地址,那确实没啥用;但套接字 API 要支持 IPv4/IPv6/Unix 域等,就必须有它

默认情况下:UDP 的 recvfrom 就是"来着不拒"。 只要数据报的目标 IP 和端口匹配你的套接字,不管来源是谁 (任何源 IP、任何源端口),内核都会把它放进该套接字的接收队列。recvfrom 只是从队列里取一个出来,并把来源地址填到 src_addr 里告诉你。并给我们对方也就是 client 的socket 信息(ip和port)。

3.4 sendto 发送消息

cpp 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
            const struct sockaddr *dest_addr, socklen_t addrlen);

sockfd 告诉内核"用自己 哪个套接字发数据"。buf 指向要发送的数据 的缓冲区。len 要发送的数据长度(字节数)。flags 发送标志,通常填 0,阻塞式。dest_addr 指向目标地址的结构体指针,因为发送消息前要先知道服务端ip和addr,并且套接字 API 类型是 IPv4/IPv6/Unix 接收方区分的,自己不用在这里填写自己的信息。len 是结构体的实际大小。成功返回实际发送字节数,失败返回 -1。

每个套接字(socket)都有自己独立的发送缓冲区和接收缓冲区。 阻塞(Blocking)描述的是:当某个操作无法立即完成时,程序是"停下来等"还是"立刻返回一个错误"。

netstat -anup a:all n:num u:udp p:process。 Foreign Address 0.0.0.0:*表示接收任何客户端的请求。n的作用是把能显示成数字的都显示成数字,例如以数字方式显示出localhost的ip地址。

服务器提供服务需要提供自己的 ip和端口号。**客户端一定也需要绑定自己的套接字和网络信息,但是不需要显示绑定bind,**IP 也不用写 ,内核会自动帮你填。client会在首次发送数据的时候会自动进行绑定,不用手写。client 也需要端口,但我们希望它绑定随机端口。自己也可以绑定,但是非常不推荐,因为有很多客户端程序,当使用不同服务时可能因为要使用同一个端口发生冲突。但 服务端的端口号一般是众所周知,不改变的

127.0.0.1 :本地回环,它代表"本机自己",通常用来进行网络cs(client,server在一台机器)的测试。数据不离开本机,整个 127.0.0.0/8 网段都是回环,前 8 位是固定的网络号,剩下的 24 位可以随意变化,只能进行本地通信。

所以进程间本地通信也可以使用网络通信127.0.0.0/8。

可以发现客户端已经自动进行了绑定端口号。

我们服务端使用云服务器不能绑定云服务器提供的公网IP,因为云服务器的公网 IP 根本不在你的服务器网卡上,而在云服务商的网关上 ,我们的的服务器只拥有一个内网 IP, 可以通过(ip addr)查看内网 ip,因为 bind 只能绑定"你这台机器网卡上真实配置的 IP"。 云服务器的公网 IP 不在你的网卡上,所以内核会直接拒绝。如果是真的linux 环境,可以直接 bind IP,但我们强烈不推荐给服务器 bind 固定的IP,因为如果绑定了且有两张网卡就只能收到一个 socket 的信息,所以更推荐 服务端本地任意 IP 绑定的方式,只要端口相同,都会给绑定这个端口的进程:

cpp 复制代码
//ip的动态绑定
local.sin_addr.s_addr = INADDR_ANY;

再次运行可以发现 IP 地址变成了 0.0.0.0,表示所有 IP 都可以。UDP 是无连接的,服务器 bind 到0.0.0.0:8888 后,没有固定对端,所以 Foreign Address 是 0.0.0.0:* 表示可以接收任意IP和端口。

云服务器,大部分的端口号是被腾讯云,阿里云拦截的。我们要能够进行数据的发送,需要开发指定的多个端口,需要开发指定的多个端口 8080,8181,8888。怎么开放?云服务器的网络后端,防火墙,安全组中开放。

0.0.0.0 允许所有地址,ALL表示是 UDP,TCP 所有端口。

4. 简单的应用

26/9 · 哈哈/linux_Tencent - 码云 - 开源中国一共有三个udp程序。

  1. windows 充当 client,实现简单的 echo server,服务器收到什么就返回什么。

windows端socket,接口与linux端差别不会很大,接口几乎完全一样,因为TCP/IP 协议是在内核中的所以 windows linux 都是类似的。

windows代码:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>

#pragma warning(disable:4996)//禁掉这个错误
#pragma comment(lib, "ws2_32.lib")//使用这个库


uint16_t serverport = 8888;//自己选端口
std::string serverip = "0.0.0.0";//自己的云服务器ip

int main()
{
	//差别1
	WSADATA wsd;//windows 套接字
	WSAStartup(MAKEWORD(2, 2), &wsd);//选择2.2版本名称。固定写法,初始化库
	
	struct sockaddr_in server;
	memset(&server, 0, sizeof(server));

	server.sin_family = AF_INET;//与linux相同
	server.sin_port = htons(serverport);//?
	server.sin_addr.s_addr = inet_addr(serverip.c_str());
	
	SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sockfd == SOCKET_ERROR)
	{
		std::cout << "socker error" << std::endl;
		return 1;
	}

	std::string message;
	char buffer[1024];
	while (true)
	{
		std::cout << "Please Enter@ ";
		std::getline(std::cin, message);
		sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&server, sizeof(server));//c++中struct 可以省略,像class

		struct sockaddr_in temp;
		int len = sizeof(temp);
		int s = recvfrom(sockfd, buffer, 1023, 0, (sockaddr*) &temp, &len);
		if (s > 0)
		{
			buffer[s] = 0;
			std::cout << "server echo# " << buffer << std::endl;
		}
	}

	//差别2
	closesocket(sockfd);
	WSACleanup();//固定写法,清理库
	return 0;
}

2. 客户端输入命令,服务端将处理结果返回。使用了popen函数。

cpp 复制代码
#include <stdio.h>
FILE* popen(const char* command, const char* type);
int pclose(FILE* stream);
  1. 底层自动帮我们创建管道
  2. fork()为我们创建子进程
  3. 让子进程程序替换执行命令command

结果返回到FILE*中,type表示将来对返回值文件的读取方式。失败返回NULL。如果命令执行有结果,也可以访问文件查看。pclose 关闭文件流。

std::bind 的参数分两种命运:

bind 里写的 命运 是否出现在最终签名
具体值_sockfdbuffer 立即固定,被"吃掉" ❌ 不出现
占位符_1_2 留个坑,等调用时填 ✅ 出现

clear只会清理client的屏幕,因为控制序列只有直接送到终端才会生效,而 ls 会直接 把结果(文件名列表)写到 stdout, 这里 stdout 是 popen 创建的管道,不是终端

3. 简单的聊天室,udp 是全双工的,可以一个线程读,一个线程写,不会互相干扰。

任务是一个类对象,函数也是对象,所以可以任务可以是仿函数,也可以是函数。每有一条消息,进程池中的进程就会竞争式的转发消息。

云服务器如何将文件运输到本地:

命令 全称 方向 用途
sz s end ZMODEM 远程 → 本地 把远程服务器上的文件下载到本地
rz r eceive ZMODEM 本地 → 远程 把本地文件上传到远程服务器

记忆:sz 是"送出去",rz 是"收进来"(都是从远程服务器的视角)。

scp 是 Secure Copy 的缩写,一个基于 SSH 协议的加密文件传输命令。它可以在本地和远程主机之间,或者两台远程主机之间复制文件/目录。因为走 SSH 通道,所以传输是加密的

scp 选项 源路径

  • 目标路径本地路径直接写,如 /home/file.txt
  • 远程路径写成 用户名@主机IP:路径,如 wdz@106.55.106.178:/home

本篇结束!

相关推荐
其实防守也摸鱼1 小时前
堆叠注入(Stacked Injection)详解
服务器·数据库·windows·https·ssl
郝学胜-神的一滴1 小时前
C++20模板元编程 05:吃透变参模板,解锁编译期万能参数能力
服务器·开发语言·c++·windows·vscode
Wang's Blog2 小时前
Java 接入Redis: 五大数据类型与存储结构选型
java·服务器·redis
Wang's Blog2 小时前
Java 接入Redis: 字符串与哈希类型操作命令
java·服务器·redis
是店小二呀3 小时前
鸿蒙PC_Dart-Sass-ohos适配全记录
linux·运维·windows
菜根Sec4 小时前
没钱考网络安全证书了
网络·网络安全·信息安全·信息安全工程师·网络安全公司
智能运维指南10 小时前
代码管理软件如何融入 DevOps 工具链?全链路联通的 5 个关键点
运维·devops·信创适配·嘉为蓝鲸
充电zcx10 小时前
Linux:4:开发工具详解
linux·运维·服务器
筼筜10 小时前
【系统架构设计师】通信系统架构设计理论与实践
网络