> 技术文档 > (udp)网络编程套接字Linux(整理)_第1关:udp套接字创建与端口绑定

(udp)网络编程套接字Linux(整理)_第1关:udp套接字创建与端口绑定


目录

源IP地址和目的IP地址

认识端口号

理解 \"端口号\" 和 \"进程ID\"

理解源端口号和目的端口号

IP:Port()

认识TCP协议

认识UDP协议

网络字节

网络字节序和主机字节序的转换

主机转网络

​编辑

​编辑

网络转主机(后面ntoa就不建议使用了)​编辑

网络字节序和主机字节序的转换函数

示例代码

解释

输出示例

适用平台

总结

网络转主机序列,主机序列转网络序列

地址转换函数(字符串传4字节IP)

套接字编程的种类:

udp代码示例

socket(创建一个套接字)

​编辑

绑定bind​编辑

绑定端口号

sockaddr结构

bzero把上面的结构体清空

INET

addr,ip地址(32位的),port端口号,family所用的域或者家族(AF_INET)

int,uint16_t,uint32_t的区别及其各自的用法

在网络中为什么用uint16_t,不用int

##的作用

主机序列转成网络序列

main中这样传ip

如何快速的将整数IP字符串IP

把字符串风格的ip地址转化为四字节(网络风格的四字节)

以上udp服务器核心的启动代码基本已经完成

思路精华

服务器应该周而复始的运行

recv

收消息

发送回给客户端

查看端口号和IP地址(netstat -naup)​编辑

测试,ip地址是云服务器的,8080端口号

云服务器最好就直接不写ip地址就好了,直接默认的0

关于port的问题(端口号)

0-1023不让绑定,最好绑定1024的端口,有的3306也不可以

写成命令行./udpServer+port

写一个客户端

创建套接字

记得close网络文件

客户端要bind吗

服务器的端口号要确定是因为,客户端是要访问服务端的,需要知道确定的端口号和ip

客户端写成命令行./udpServer+ip+port

1.数据 2.发给谁(sendto)

整理一下。主机转网络(服务器信息)

接收一下(recvfrom)

代码示例

测试(客户端向服务器发信息)

udpServer.hpp

udpClient.cc

main.cc

makefile

多台机器网络信息传递测试

网络处理数据和接收数据耦合度太高了

服务器外面给接收到的数据做处理

如果发来的是命令呢

本地环回127.0.0.1

在服务器查看发信的客户端的端口号和IP

网络转主机拿到端口号和IP

改变一下处理数据方式,打印出IP和port

只允许一个客户端连接/判断是不是一个新用户(kv)

客户端整改

登录qq时,不发送信息也能收到信息,但目前的客户端没法做到

会在getline阻塞住

客户端改成多线程,一个线程输入一个线程显示

udp的sockfd是可以同时被读写的

是线程安全的

思路和测试结果​编辑

制作简单的聊天室类似群聊

覆盖问题


源IP地址和目的IP地址

唐僧例子1

  • 在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
  • 思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析.

认识端口号

5aa466f7741c43a6a19bacf33aabd297.png

理解 \"端口号\" 和 \"进程ID\"

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这 两者之间是怎样的关系?

10086例子

另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;fe650b7d258a451db453b5bfa3ed4483.png216566e140c14ef1a5b81f7c0b77b650.png

理解源端口号和目的端口号

唐僧例子2

送快递例子

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 \"数据是谁发的, 要 发给谁\";

IP:Port()

ip地址4字节 端口号两个字节

跨主机

7f46fb8563ff448bbee7b5773dd222f6.png

看到的公共部分 是网络

e63b13bff86441209aa88a58a35e44a9.png

在公网上:
IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程
IP:Port=标识全网唯一的一个进程

9ed020d12d3c41fea1c69616d95c77b8.pngfe650b7d258a451db453b5bfa3ed4483.png216566e140c14ef1a5b81f7c0b77b650.png

认识TCP协议

68e77c3741a24c249575602bfb9c54e6.png

认识UDP协议

cb6e6a1c88d24b698fd2049578f21457.png

网络字节序

ip地址4字节 端口号两个字节

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

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

a8346459c5d84253b9f3e6010f9f7c41.png

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

2c926d5138e3446ba4c12cb6e023c15c.png

480c8a69229b485982c3aef641c8087d.png

网络字节序和主机字节序的转换

主机转网络

服务端

客户端

网络转主机(后面ntoa就不建议使用了)

在 C 或 C++ 中,网络字节序和主机字节序的转换非常重要,特别是在进行网络编程时,因为不同的平台可能有不同的字节序(Endianness)。网络字节序通常是大端字节序(Big Endian),而主机字节序可能是大端或小端(Little Endian)。

网络字节序和主机字节序的转换函数

  1. **htons()**:将主机字节序(Host)转换为网络字节序(Network),适用于 16 位数据(short)。
  2. **htonl()**:将主机字节序(Host)转换为网络字节序(Network),适用于 32 位数据(long)。
  3. **ntohs()**:将网络字节序(Network)转换为主机字节序(Host),适用于 16 位数据(short)。
  4. **ntohl()**:将网络字节序(Network)转换为主机字节序(Host),适用于 32 位数据(long)。

示例代码

#include #include  // 包含字节序转换函数(适用于 Linux 和 UNIX)int main() { uint16_t host_short = 0x1234; // 主机字节序的 16 位数据 uint32_t host_long = 0x12345678; // 主机字节序的 32 位数据 // 主机字节序转网络字节序 uint16_t network_short = htons(host_short); uint32_t network_long = htonl(host_long); // 网络字节序转主机字节序 uint16_t converted_short = ntohs(network_short); uint32_t converted_long = ntohl(network_long); // 输出结果 printf(\"Host short: 0x%04X, Network short: 0x%04X, Converted back: 0x%04X\\n\",  host_short, network_short, converted_short); printf(\"Host long: 0x%08X, Network long: 0x%08X, Converted back: 0x%08X\\n\",  host_long, network_long, converted_long); return 0;}

解释

  • **htons()**:将主机字节序的 16 位数据(host_short)转换为网络字节序。
  • **htonl()**:将主机字节序的 32 位数据(host_long)转换为网络字节序。
  • **ntohs()**:将网络字节序的 16 位数据(network_short)转换为主机字节序。
  • **ntohl()**:将网络字节序的 32 位数据(network_long)转换为主机字节序。

输出示例

Host short: 0x1234, Network short: 0x3412, Converted back: 0x1234Host long: 0x12345678, Network long: 0x78563412, Converted back: 0x12345678

适用平台

这些字节序转换函数通常用于 Linux 和 UNIX 系统中,通过包含arpa/inet.h头文件来访问。对于 Windows 系统,提供了类似的转换函数,但位于winsock2.h中,使用方法相同。

总结

  • htons()和 **htonl()**:用于将主机字节序转换为网络字节序(适用于 16 位和 32 位数据)。
  • ntohs()和 **ntohl()**:用于将网络字节序转换为主机字节序。

这些转换函数在进行网络编程时非常重要,可以确保不同平台和字节序的主机之间进行正确的数据传输和处理。

网络转主机序列,主机序列转网络序列

发送到网络里的要转网络序列,从网络里拿的要转主机序列

地址转换函数(字符串传4字节IP)

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址

但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。 代码示例:

关于inet_ntoa

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是 否需要调用者手动释放呢

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.

那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

  • 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

套接字编程的种类:

1.域间套接字编程2.原始套接字编程3.网络套接字编程

d64c03e49b54459a82f8462e3fa411a3.png

udp代码示例

udp服务器

ca4091b5be82448daecdf81c49d6f1dd.png

socket(创建一个套接字)

socket创建套接字

第一个参数是一个域

socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)int socket(int domain, int type, int protocol); // 绑定端口号 (TCP/UDP, 服务器) int bind(int socket, const struct sockaddr *address, socklen_t address_len); // 开始监听socket (TCP, 服务器)int listen(int socket, int backlog); // 接收请求 (TCP, 服务器)int accept(int socket, struct sockaddr* address, socklen_t* address_len); // 建立连接 (TCP, 客户端)int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

第一个参数:创建一个套接字的域,什么叫做域呢

可以理解为我们所要的那个套接字他是属于什么AF_INET,将来是使用IPv4还是IPv6网络通信的,还是有叫本地通信也叫作域间通信,第二个参数是socket对应的类型,udp是面向用户数据报(SOCK_DGRAM)tcp是面向字节流的第三个参数不用填,协议类型,创建一个套接字的本质就是打开一个文件,必须要告诉服务器,端口号,然后再绑定套接字端口号

8d653c031bf046d5aa810558107d2b7f.png

不同类型(第一个参数的)

8716260b5598498d938bbcbd53f4783a.png

第二个:定义的套接字的类型

30f91d16c7da430a8bff696543150763.png

第三个写0就可以

71ccbde87cf1426295bbaf65c70b00e8.png

#pragma once#include#include  #include using namespace std;class UdpSever{public: UdpSever() {} void Init() { sockfd = socket(AF_INET, SOCK_DGRAM, 0); } void Run() {} ~UdpSever() {}private: int sockfd;//网络文件描述符};

把日志功能拷贝进来

99144e4d1376478fa96baba7ecefd7b8.png

创建一个Log对象

8b5540f7b349445d89640824c509a900.png

740be8267ae442d49fd83f34c6ec5278.png

std::unique_ptr3d9a64f6500d4915b22a6f79297d3eb5.png

c04034a05b5349a4b2afd45c2866be76.png

C++11中开始提供更靠谱的unique_ptr

文档:https://cplusplus.com/reference/memory/unique_ptr/

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原 理

/ C++11库才更新智能指针实现

// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr // C++11将boost库中智能指针精华部分吸收了过来

// C++11->unique_ptr/shared_ptr/weak_ptr // unique_ptr/scoped_ptr // 原理:简单粗暴 -- 防拷贝

测试:

3b54eddb23834907af8c46e9fd808acc.png

ada8892b420b4ebc89d44d8d5485ce29.png

绑定bindc4c721283d374ddf95eddc091e2fabe0.png

ba21e2d038d646c2ad598624e655f350.png

绑定端口号

sockaddr结构

3355b591bfbc4a36ae4e65a4883999d1.png9f566ed4b506466d93ba183f949e7e0a.png

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好 处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构de0c98c98fdf4063a5893be71629af69.png

sockaddr_in 结构

e610f6c82eff42288335ea95c0ed089f.png虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主 要有三部分信息: 地址类型, 端口号, IP地址.

in_addr结构

012fc56492ba47a8845ceffec8cb05ff.pngin_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;

bzero把上面的结构体清空

5c80c4518d9f4ade955e831260500fbf.png

f051463c366842ca9deedd4019d2984b.png

INET

6b5b0f5524774deb843fc63d04ab5c15.png

addr,ip地址(32位的),port端口号,family所用的域或者家族(AF_INET)

c0525808bd5142f8855f07c36eb7734a.png

int,uint16_t,uint32_t的区别及其各自的用法

intuint16_tuint32_t都是表示整数类型的数据类型,它们在表示范围和用途上有所不同。下面是它们的区别和常见的用法:

1.int

  • 定义int是C/C++中的标准整数类型,通常用于表示整数。它的大小和范围依赖于平台(操作系统和硬件)。

  • 大小和范围

    • 在大多数现代平台上,int通常是 32 位的(4 字节),但是也有一些平台会使用 16 位(2 字节)或 64 位(8 字节)来表示。

    • 在 32 位系统上,int的范围通常是:-2,147,483,6482,147,483,647

    • 在 64 位系统上,int的范围一般相同,但可能会有所不同,具体取决于编译器和操作系统。

  • 用途:适用于一般整数的存储,特别是当你不需要指定特定位数的整数时。通常是默认的整数类型。

2.uint16_t

  • 定义uint16_t是一个固定宽度的无符号整数类型,表示一个 16 位(2 字节)的无符号整数。它是通过 C99 标准中的头文件定义的。

  • 大小和范围

    • 它的大小是固定的 16 位(2 字节)。

    • 范围是065535(即2^16 - 1)。

  • 用途:用于需要 16 位无符号整数的场景,适用于存储比较小的正整数值,节省内存空间,常用于协议数据、传感器数据、颜色值等场景。

3.uint32_t

  • 定义uint32_t是一个固定宽度的无符号整数类型,表示一个 32 位(4 字节)的无符号整数。它同样是通过头文件定义的。

  • 大小和范围

    • 它的大小是固定的 32 位(4 字节)。

    • 范围是04,294,967,295(即2^32 - 1)。

  • 用途:用于需要 32 位无符号整数的场景,常用于表示较大范围的正整数,如文件大小、内存地址、网络协议等。

总结对比:

数据类型

大小

范围

用途

int

32 位(常见)

-2,147,483,6482,147,483,647

通用整数类型,平台相关

uint16_t

16 位

065535

用于小范围无符号整数,如协议、传感器数据

uint32_t

32 位

04,294,967,295

用于大范围无符号整数,如文件大小、网络地址

选择依据:

  • 如果你知道需要存储的数值范围,并且希望节省内存,可以选择uint16_tuint32_t

  • 如果不关心存储大小且需要处理负数,int是默认选择。

  • 需要无符号整数时,uint16_tuint32_t是更为合适的选择。