
于此止?:个人主页
本文选自专栏《Linux网络》
❄️个人专栏传送门:《Linux篇》 《QT百日筑基篇》 《算法篇》
《C++与STL剖析篇》 《数据结构篇》 《Python基础篇》 《嵌入式砺剑录》
⭐️纵有狂风拔地起,我亦乘风破万里⭐️

Socket的介绍与使用
前言:本篇文章先与上一篇文章有密切的关系, 我们会将一些基础进行详细的讲解, 并且讲解完基础我们会转向讲解对应的socket对应的接口的使用,最后在讲解原理。
目录/索引
目录
Socket的介绍与使用
目录/索引
一、Socket编程预备
1.理解源IP地址和目的IP地址
2.网络通信的本质
二、认识端口号(port)
底层的实现
端口号的范围划分
理解源端口号和目的端口号
三、理解socket(套接字)
四、传输层代表
TCP(Transmission Control Protocol,传输层控制协议)
UDP(User Datagram Protocol,用户数据报协议)
可靠 vs 不可靠
五、网络字节序
复习我们的大小端
字节序的讲解
六、socket变成通信接口
socket常见的API
sockaddr结构
socket函数的解析
七、使用UDP进行网络通信
bind
recvfrom
serve.hpp
serve.cc
client.hpp
client.cc
commit.h
makefile
效果的展示
一、Socket编程预备
1.理解源IP地址和目的IP地址
简单来说我们的源IP是我们哪个机器发的信息,定位标识我们机器的唯一性,我们的目的IP是说我们要将我们的信息发给那一个主机。
-在网络中,我们的IP地址是用来表示我们主机的唯一性的。
-IP地址是有很多的分类的。
这里我们会有一个问题: 数据传输到主机是目的吗? 答:不是。数据是给人进行使用的。
例如:聊天是人在聊天,下载是人在下载, 浏览是人在浏览。
但是认识怎么进行上面的操作呢?答有对应的软件, 如果我们想要聊天有对应的app(QQ,wx等聊天软件),我们想要进行下载有对应的应用商店, 我们想要浏览有对应的浏览器。
而我们在系统的部分知道了, 我们看似是我们启动了这些软件, 起初是我们点击了对应的app图标创建了对应的进程,是进程将我们这些App打开了。所以我们启动的这些都是进程,换句话说进程是人在OS中的代表(或者指挥官),我们创建对应的进程进行对应的操作,我们对应的操作会产生对应的数据,只要把数据给了我们的进程,进而就相当于我们人拿到了对应的数据。所以我们人打开APP的时候就可以理解为我们获取了APP中的数据。
所以, 数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程才是目的。
但是在系统中,同时会存在非常多的进程, 当数据到达目标主机之后,怎么转发给目标进程?这就是要在网络的背景下, 在系统中,标识主机的唯一性(ip)。
当然这里我们还能这么的进行理解: 我们进程在操作系统是人的代表,我们在将数据给我们对应的进程之后,我们人也就相当于得到了这一份资源。
我们在使用网络无非就只有两种情况:
1.从远端服务器获得数据(从gitee上拉取仓库到本地)。
2. 将我们本地的数据推送到远端(类比gitee)。
这两种会组成一下的两种关系:
进程(内存) <——> 网卡 <——> 网络
这种关系的本质不就是IO吗,我将远端的数据拷贝到本地,将本地的数据拷贝到远端。

2.网络通信的本质
下面我们通过一张图来看一下网络通信的本质
再次之前我们是没有学习过网络通信的但是我们学习过进程间的通信。我们的网络通信也是一样的,让我们的不同进程看到同一份资源,也就是我们两个不同的进程进行数据交互。

我们在我们之前的进程间的通信我们的匿名管道是有血缘关系的,也就近似我们拿到了对方的pid我们能进行通信的。我们在命名管道的时候我们采用了一种叫做我们的key来标识我们结构的唯一性,我们在共享内存是通过我们的虚拟物理空间的映射进行的资源的访问。他会给我们返回一段虚拟内存空间,这段空间也是被保护在vm_area_struct中他返回的地址也是具有唯一性的。所以我们要想我们的网络也进行我们对应的通信我们也要使用一个标识符来标识我们网络通信的唯一性。
当然我们网络也是一个进程,他后面也会创建一个进程来执行我们对应的操作。我们的进程有pid那么我们能不能使用pid来标识我们网络通信的唯一性呢???
答案:不能,因为我们后面如果我们的pid变化了,那我们的网络也要跟着变化,这样我们的耦合就大了,我们的操作系统一直追随的就是低耦合高内聚,所以我们使用pid的方案并不是那么好所以我们要创建只属于我们网络的标识符。
当然我们网络也是有的我们称之为端口号。
二、认识端口号(port)
首先我们的端口号是我们传输层协议的内容。用来标识我们网络中的进程。
1.端口号是一个2字节的16位整数;
2.端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来进行处理;
3.IP地址+端口号能够标识网络上的某一个主机的某一个进程;
4.端口号只能被一个进程占用。
5. 但是一个进程特殊情况下也能拥有多个端口号。

底层的实现
在我们底层实现原理,我们有一个个进程我们的用户层传给我们的传输层的时候我们前面会封装一个报文,这个报文里面的内容是我们对应的想要给哪个进程发送信息的该进程网络端口号,我们在我们的传输层有一个哈希表里面记录着我们的端口号,我们拿着我们对应的端口号直接就能找到我们对应的发送到消息的进程,我们直接将我们有效载荷传给我们对应端口号指向的进程。

端口号的范围划分
当然像我们进程中有一些特殊的pid是在我们内核启动的时候就存在了的。还有我们现实生活中的一些电话像(110,119)也是也特殊的用途。当然我们的端口号也是不例外的。我们也是有对应的端口号被占用的情况。
0-1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的。
1024-65535:操作系统动态分配的端口号,客户端程序的端口号,就是在这个范围进行分配的。
为什么是到我们的65535,因为我们的端口号是我们的2字节的16位整数。

我们换算到10进制正好是65535。
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段有两个端口号,分别叫做源端口号和目的端口号,就是在描述“数据是谁发的,要发给谁”;
三、理解socket(套接字)
说起socket它的中文意思是有点意思的叫做插排。

1.综上,IP地址用来标识我们互联网中的唯一一台主机,我们的port(端口号是用来标识我们唯一的网络进程)。
2.IP+Port就能标识我们呢互联网中的唯一进程。
3.所以,通信的时候,本质是两个互联网进程的代表人来进行通信,{srclp,srcPort,dstlp,dstPort}这样的4元组就能标识互联名网的唯二进程。
| srcIP (或 srclp) | Source Internet Protocol Address | 源IP地址 | 发送数据包的那台设备的网络地址。它标识了“数据从哪里来”。 | 192.168.1.100 (家庭电脑内网IP) |
| srcPort | Source Port Number | 源端口号 | 发送端应用程序所绑定的端口号。它标识了“数据由本机的哪个程序发出”。客户端通常使用临时端口(1024-65535)。 | 54321 (随机分配的高位端口) |
| dstIP (或 dstlp) | Destination Internet Protocol Address | 目标IP地址 | 接收数据包的那台设备的网络地址。它标识了“数据要去哪里”。 | 8.8.8.8 (谷歌DNS服务器) |
| dstPort | Destination Port Number | 目标端口号 | 接收端应用程序所监听的端口号。它标识了“数据到达对方后,该交给对方的哪个程序处理”。通常使用知名端口(0-1023)或注册端口。 | 443 (HTTPS加密网页服务 |
4.所以,我们网络的本质,也是我们进程间的通信。
5.我们把ip+port叫做套接字socket。
6.socket = ip+port。
四、传输层代表
如果我们了解了系统,也了解了我们的网络协议栈,我们就会很清楚,传输层是属于内核的,那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,而传输层系统调用,是来进行网络通信的。
当然我们可以通过调用我们的TCP和UDP来进行我们对应的系统调用来进行访问我们的传输层。

当然这里的UDP和TCP不一样,那我们现在来看看他们之间的特点吧。
TCP(Transmission Control Protocol,传输层控制协议)
我们现在只对我们的TCP有一个简单的认识,我们在抽丝剥茧的了解我们的TCP。
-传输层协议
-可靠传输
-有连接
-面向字节流
UDP(User Datagram Protocol,用户数据报协议)
我们现在只对我们的TCP有一个简单的认识,我们在抽丝剥茧的了解我们的UDP。
-传输层协议
-无连接
-不可靠传输
-面向数据报
首先我们先看一下两者的对比,这两着都是传输层协议,我们的TCP是有连接的,而我们的UDP是无连接的,这两个协议最大的区别是我们的TCP是面向我们字节流的,而我们的UDP是面向我们的数据报的。这两个我们在此不做过多的解释我们给出一张对比表格,关键是我们的可靠和不可靠。
| 传输单位 | 字节流:数据被视为连续的、没有边界的字节序列。 | 数据报:每个数据包都是独立的,有明确边界。 |
| 数据边界 | 无:发送端多次发送的小数据包,可能会在接收端被合并成一个大的数据块(粘包问题)。 | 有:发送端发一个数据报,接收端就必须完整地读取一个数据报。 |
| 可靠性 | 可靠:提供确认重传机制,保证数据完整、有序、不重复地到达。 | 不可靠:“尽力而为”地交付,不保证顺序,可能丢包或重复。 |
| 连接性 | 面向连接:传输前需建立连接(如TCP的“三次握手”)。 | 无连接:可直接发送数据,无需预先建立连接。 |
| 传输效率 | 较低:因为要保证可靠性,机制复杂,头部开销大,速度相对较慢。 | 较高:机制简单,头部开销小,传输速度快,实时性强。 |
| 典型协议 | TCP (传输控制协议) | UDP (用户数据报协议) |
| 应用场景 | 对数据完整性要求高的场景,如网页浏览(HTTP/HTTPS)、文件传输(FTP)、邮件(SMTP)等。 | 对实时性要求高、可容忍少量丢包的场景,如视频通话、在线游戏、DNS解析等 |
可靠 vs 不可靠
这里的可不可靠我们不能以贬义词来进行对应的了解,我们要以中性的词来进行了解,我稍微一看肯定是可靠它不会丢失我们对应的数据,一看就很好,这样为什么要存在我们的UDP呢?它不可靠可能会丢失一部分的数据。
我们这里的可不可靠都有对应的优势,他们本质没有好和坏之分的。
我们的可靠,优点:数据不会丢失,缺点:它的实现肯定会很复杂,效率也不一定会很高。工作多占用资源多。
我们的不可靠,优点:我们的实现会简单一些,我不用太复杂的实现,只要我的数据能发送过去就可以了。缺点:数据丢失我不管。数据可能会丢失。
五、网络字节序
复习我们的大小端
我们的小端存储 
我们的大端存储

地权值位——>小权值位
低地址——>小地址
小端(简记为小小小)。
字节序的讲解
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;(有了我们这样的要求我们的大端存储会更满足我们的观感。我们上面的从低地址—>高地址依次是12345678)。
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送 / 接收数据;
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可;(我们在网络规定我们发送的主机必须是大端)。
如果不是我们对应的大端我们就要进行转换,我们读取的数据要是小端流出要转化为大端。

将0x1234abcd写入到以0x0000开始的内存中,则结果为
表格
| 0x0000 | 0x12 | 0xcd |
| 0x0001 | 0x23 | 0xab |
| 0x0002 | 0xab | 0x34 |
| 0x0003 | 0xcd | 0x12 |
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#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);
- 这些函数名很好记,h 表示 host , n 表示 network , l 表示 32 位长整数,s 表示 16 位短整数。
- 例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
🔴 网络规定
- 所有发送到网络上的数据,都必须是大端的!
六、socket变成通信接口
socket常见的API
我们现在来看看我们对应的socketAPI索要传的参数。
我们发现我们大部分的接口都是要我们的一个结构体的struct sockaddr这个结构体,我们没见过这个结构体,这个结构体是什么呢?我们来看一看吧。
// 创建 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);
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层的网络协议,如IPv4、IPv6以及后面要说的UNIX Domain Socket。然而,各种的网络协议的地址格式并不相同。
那我们来看看我们的结构吧。
这里我们要注意我们之前学习我们进程间的通信的时候我们的进程间的通信(system-v)仅支持我们的本地通信。
而我们的socket(posix)是支持我们的本地通信和我们的网络通信的。这时候我们需要创建2个接口开分别实现我们的网络通信和本地通信。那么我们的开发者不想那么的麻烦,我们的祖师爷想要实现一个接口来进行我们这2种/多种情况的调用。那么就有了下面的版本。
我们在通信前一定要进行创建我们的struct sockaddr这个结构体,这个结构体的前16位是用来标识我们的地址类型,当前是网络通信还是本地通信。如果我们传入的是AF_INET那就代表这个通信是我们网络通信,如果是AF_UNIX那就代表我们本地通信(看后面的108字节的路径名也类似我们的共享内存)。 获得我们对应的地址类型就能得到我们对应的通信类型。
我们的网络通信我们会有多存储的16位端口号(标识网络进程的唯一性)
32位的IP地址(标识主机的唯一性),8字节的填充(目的是为了进行我们的内存对齐)。

- 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 结构体指针做为参数;
我们将我们的图片的位置进行调换
我们发现这不就是我们对应的继承吗,用c实现我们对饮的继承。我们这样就能理解了,我们在传参的时候可以在cpp中的继承一起进行理解,我们在传参的时候我们对应的参数会进行切片,将我们的前面的数据进行保存(为什么:因为父进程有)我们在继承中只能子传父。
这不就是用C语言实现我们的继承和多态吗?

socket函数的解析
我们先查看以下我们的手册,看看我们对应的函数的原型。
man socket
我们能看到这个函数的一些信息

函数原型
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
我们能看到我们这里主要是由三个参数。
参数解析:
domain:域,本地/网络通信,创建通信的一端。
我们使用的网络一般都是IPv4所以我们的第一个参数设置为;
| AF_INET | IPv4 互联网协议 |
我们能传的有以下的参数

| AF_UNIX | 本地通信(进程间) | unix(7) |
| AF_LOCAL | AF_UNIX 的同义词 | — |
| AF_INET | IPv4 互联网协议 | ip(7) |
| AF_AX25 | 业余无线电 AX.25 协议 | ax25(4) |
| AF_IPX | Novell 的 IPX 协议 | — |
| AF_APPLETALK | AppleTalk 协议 | ddp(7) |
| AF_X25 | ITU-T X.25 / ISO-8208 协议 | x25(7) |
| AF_INET6 | IPv6 互联网协议 | ipv6(7) |
| AF_DECnet | DECnet 协议套接字 | — |
| AF_KEY | 密钥管理协议(最初用于 IPsec) | — |
| AF_NETLINK | 内核用户接口设备 | netlink(7) |
| AF_PACKET | 底层数据包接口 | packet(7) |
| AF_RDS | 可靠数据报套接字(RDS)协议 | rds(7), rds-rdma(7) |
| AF_PPPOX | 通用 PPP 传输层(用于 L2TP 和 PPPoE 隧道) | — |
| AF_LLC | 逻辑链路控制(IEEE 802.2 LLC)协议 | — |
| AF_IB | InfiniBand 原生寻址 | — |
| AF_MPLS | 多协议标签交换 | — |
| AF_CAN | 控制器局域网(汽车总线)协议 | — |
| AF_TIPC | TIPC(“集群域套接字”)协议 | — |
| AF_BLUETOOTH | 蓝牙底层套接字协议 | — |
| AF_ALG | 内核加密 API 接口 | — |
| AF_VSOCK | VSOCK(原 VMware VSockets)虚拟机与宿主机通信 | — |
| AF_KCM | 内核连接多路复用器接口 | — |
| AF_XDP | 快速数据路径(XDP)接口 | — |
我们的第二个参数是我们的type
type:类型/套接字,我们一般有两种1.我们的UDP,2.TCP。
我们现在主要使用我们的UDP先做我们通信,我们的UDP是我们可以有数据的丢失,他简单所以我们先使用我们对应的UDP来实现我们对应的套接字。我们的UDP是支持数据报的。
| SOCK_DGRAM | 支持数据报(无连接、不可靠、固定最大长度的消息)。 |
我们主要传递以下的参数。

表1:基础套接字类型(type 主值)
| SOCK_STREAM | 提供有序、可靠、双向、基于连接的字节流;可能支持带外数据传输机制。 |
| SOCK_DGRAM | 支持数据报(无连接、不可靠、固定最大长度的消息)。 |
| SOCK_SEQPACKET | 提供有序、可靠、双向、基于连接的数据报传输路径(固定最大长度);每次读操作必须完整读取一个数据包。 |
| SOCK_RAW | 提供原始网络协议访问(可操作底层报文)。 |
| SOCK_RDM | 提供可靠数据报层,但不保证顺序。 |
| SOCK_PACKET | 已废弃,不应用于新程序;请参阅 packet(7)。 |
表2:可选的标志位(与 type 按位或)
自 Linux 2.6.27 起,type 参数除了指定主类型外,还可以通过按位或(|)包含以下标志,以改变 socket() 的行为:
| SOCK_NONBLOCK | 在新文件描述符上设置 O_NONBLOCK 文件状态标志,避免额外调用 fcntl() 来设置非阻塞。 |
| SOCK_CLOEXEC | 在新文件描述符上设置执行时关闭(FD_CLOEXEC)标志,防止子进程继承该描述符(类似 open() 的 O_CLOEXEC)。 |
第三个参数
protocol:我们对应的协议。如果我们前两个参数已经能体现我们现在是哪一个协议了我们这个位置可以直接写0.
-
socket(AF_INET, SOCK_STREAM, 0); → 自动选 TCP
-
socket(AF_INET, SOCK_DGRAM, 0); → 自动选 UDP
-
socket(AF_INET6, SOCK_STREAM, 0); → 自动选 TCPv6
-
socket(AF_UNIX, SOCK_STREAM, 0); → 自动选本地流协议
| 原始套接字(抓包/构造报文) | socket(AF_INET, SOCK_RAW, **IPPROTO_ICMP**); | 指定要接收或发送的特定三层协议(如 ICMP、TCP、UDP)。常用宏:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP、IPPROTO_RAW。 |
| 数据链路层抓包 | socket(AF_PACKET, SOCK_RAW, **htons(ETH_P_ALL)**); | 必须指定以太网帧类型(如 ETH_P_ALL 抓所有包,ETH_P_IP 只抓 IP 包),且需要用 htons() 转换字节序。 |
| Netlink 内核通信 | socket(AF_NETLINK, SOCK_RAW, **NETLINK_ROUTE**); | 必须指定 netlink 协议类型(如 NETLINK_ROUTE 操作路由,NETLINK_GENERIC 通用接口)。 |
| 蓝牙或特殊总线 | socket(AF_BLUETOOTH, SOCK_STREAM, **BTPROTO_RFCOMM**); | 蓝牙协议栈必须显式指定子协议(如 RFCOMM、L2CAP)。 |
返回值:这个返回值就有趣的多了
成功返回我们对应的文件描述符,失败返回-1错误码被设置。

七、使用UDP进行网络通信
这里我们主要是实现我们之间的通信所以我们创建文件
我们的client.hpp是客户端方法的声明与实现,我们的client.cc是我们的客户端的主程序和我们方法的调用。
serve.hpp是我们的服务端的声明与实现,我们的serve是我们服务端主程序的调用。
我们的makefile是我们的自动化构建工具,我们的commit是我们要包含我们用到的头文件。

这里我们的makefile你能直接写,我们的makefile要同时编译两个文件。
makefile:
这样就能一键编译两个文件了。
.PHONY:all
all: client serve
client:client.cc
g++ -o $@ $^
serve:serve.cc
g++ -o $@ $^
.PHONY:clean
clean:
rm -rf client serve
首先我们要进行我们的网络通信必须要有我们对应的网络套接字,所以我们使用socket来创建我们对应的网络套接字,我们先进行编写我们对应的服务端吧。
我们要先想一下我们的serve要怎么实现我们的我们想要两个接口一个Init用作初始化,一个Start用作我们的开始运行我们的服务器。
所以我们对应的socket的实现一定是在我们对应的Init里面的。
这边注意我们使用我们面向对象的方式进行编写代码。
我们的serve.cpp对应的调用
#include "serve.hpp"
int main()
{
serve se;
se.Init();
se.start();
return 0;
}
我们的serve.hpp
#include "commit.h"
const int fail_fd = -1;
class serve {
public:
serve()
:soc_fd(fail_fd)
{}
void Init()
{
//创建网络套接字
soc_fd = socket(AF_INET,SOCK_DGRAM,0);
if(soc_fd < 0)
{
perror("socket fail!!!");
exit(1);
}
printf("创建成功");
}
~serve(){}
private:
int soc_fd;
};
我们创建完了要将我们的套接字进行我们的bind。所以我们这里还要认识一个接口
我们查看我们的文档
bind
man bind

函数原型:
作用:绑定一个套接字的名字。
BIND(2)
NAME
bind – bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数介绍:
socket:网络文件的文件描述符。我们在socket的时候我们的返回了一个文件描述符。这里就是将这个文件描述符填写到这里。
我们第二个参数const sockaddr*addr我们看类型非常的熟悉,不就是为了我们分辨我们是网络通信还是我们的本地通信的网络类型的吗?我们使用sockaddr是为了分辨那两个类型。

addrlen:是我们的一个参数表示我们的前一个结构体的大小。
返回值:
成功返回0,失败返回-1错误码被设置。

我们第二个参数的内部有很多不认识的变量我们转到定义查看以下。

我们的in_port_t 这个类型就是我们的一个unsigned shot int(2个字节我们的端口号也是两个字节的)我们的一个无符号的短整型这个主要是存储我们的端口号。
我们的in_addr是一个无符号的整形主要储存我们的IP地址。



我们在绑定之前我们要先进行创建我们的struct sockaddr_in这个类型的变量。
而后将我们的端口号和我们的Ip地址进行填写。然后传进去进行我们对应的绑定。
//将我们的网络套接字进行bind
struct sockaddr_in* addr;
//我们在绑定前要进行设置我们的IP和端口号
addr->sin_port = _port;
addr->sin_addr.s_addr = inet_addr(_ip.c_str());
int ret = bind(_sockfd,(struct sockaddr* )addr, sizeof(addr));
if(ret == -1)
{
perror("bind fail!!!");
exit(2);
}
printf("绑定成功");
我们要开始进行运行了,向我们的服务器我们用的软件都是不会退出的,他们的后端服务器一直在运行,那么这时候我们就要进行我们的死循环while(1)。
我们这里收消息要有我们对应的接口我们查看以下我们的官方文档
recvfrom
man recvfrom

函数原型:
作用:收到一个socket的消息。
NAME
recv, recvfrom, recvmsg – receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数的介绍:
1.sockfd是我们的网络文件的文件描述符,我们这里在使用我们的socket的时候就将我们的网络文件描述符进行了对应的返回。
2.buf相当于我们内置一个缓冲区我们这个缓冲区要放入我们读取到的数据。
3.len是我们读取多少个数据的长度。
4.flag是我们的标记位我们有以下的标记为
| 0 | 默认普通阻塞接收,和 read 行为一致 |
| MSG_OOB | 接收带外数据(TCP 紧急数据),UDP 不支持 |
| MSG_PEEK | 窥探数据:读取缓冲区数据,但内核不会把数据删掉;下次 recv/recvfrom 还能读到同样内容 |
| MSG_WAITALL | 等待凑够 len 字节才返回(不保证,信号、关闭会打断) |
| MSG_DONTWAIT | 非阻塞模式,没有数据直接返回 -1,errno=EAGAIN |
| MSG_TRUNC | Linux 特有,返回真实数据包长度,即使 buf 装不下 |
5.struct sockaddr * src: 这个参数是输出型参数,我们读取过来来标记我们的一些sockaddr的信息。
6. addrlen:即是输出型参数,又是我们输入型参数。
作为输出型参数是我们的结构体大小,输入型参数是我们的信息的大小。
返回值:
成功返回我们接受的字节数,失败返回-1.

我们的ssize是一个长整形
我们收消息就有了下面的一块了
_running = true;
char* buff[MAX_IF];
while(1)
{
struct sockaddr* src_addr;
socklen_t* addrrlen;
//收消息
ssize_t ret = recvfrom(_sockfd,buff,MAX_IF,0,(struct sockaddr* )src_addr,addrrlen);
我们还要给我们对应的文件进行发消息进行通信。
man sendto
我们查询一下我们官方的文档。
作用:给这个套接字发送一个消息。

函数原型:
NAME
send, sendto, sendmsg – send a message on a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数介绍:
sockfd:我们对应的文件描述符我们在socket的时候返回值
buf:要将发送哪里的数据。
len:数据的长度有多长
flag:
| 0 | 默认阻塞模式,无特殊行为 | ✅ | ✅ | 业务首选,绝大多数代码填 0 |
| MSG_DONTWAIT | 本次调用非阻塞;缓冲区满立刻返回 – 1,errno=EAGAIN/EWOULDBLOCK | ✅ | ✅ | 只本次 IO 非阻塞,不修改 socket 本身属性 |
| MSG_NOSIGNAL | 对已断开对端发送时,不产生 SIGPIPE 信号,错误通过返回值 + errno 返回 | ✅ | ✅ | 多线程程序很实用,避免进程被 SIGPIPE 杀死 |
| MSG_MORE | 提示内核还有更多数据,延迟发送,合并报文 | ✅ | ✅ | Linux 私有;适合连续多发,减少小包;UDP 不要乱用,不会合并多个 UDP 数据包 |
| MSG_CONFIRM | UDP:告诉内核链路可达,更新 ARP 缓存 | ✅ | ❌ | UDP 专用,极少业务使用 |
| MSG_OOB | 发送 TCP 带外紧急数据 | ❌ | ✅ | UDP 没有带外数据,传了无效 |
| MSG_WAITALL | sendto 下完全无效 | – | – | 仅 recv 系列接收函数使用 |
| MSG_PEEK | sendto 传入,无任何效果 | – | – | 仅接收方使用 |
| MSG_TRUNC | sendto 无效,仅 recv 接收侧 | – | – | |
| MSG_CTRUNC | sendto 无效,仅 recv 接收侧 | – | – |
dest_addr:我们要给谁发送我们对应的数据。
addrlen:我们要给他发送多少的数据。
返回值:
成功返回实际读到的字节数, 失败返回-1.

我们来讲解以下我们对应的编写函数的细节问题
我们来讲解一下我们编写代码的细节问题。
| htons() | Host To Network Short | 主机短整型 → 网络字节序(16bit,端口) | uint16_t(主机序端口) | uint16_t(网络序) |
| htonl() | Host To Network Long | 主机长整型 → 网络字节序(32bit,IPv4) | uint32_t(主机序) | uint32_t(网络序) |
| ntohs() | Network To Host Short | 网络短整型 → 主机字节序(16bit) | uint16_t(网络序) | uint16_t(主机序) |
| ntohl() | Network To Host Long | 网络长整型 → 主机字节序(32bit) | uint32_t(网络序) | uint32_t(主机序) |
serve.hpp
#include "commit.h"
const int fail_fd = -1; //我们文件描述符的初始化
class serve {
public:
serve(const std::string& ip,uint16_t port) //构造函数我们这需要我们的ip和我们的端口号用来表示我们的唯一性
:_sockfd(fail_fd),_ip(ip),_port(port), _running(false) //我们要初始化我们对应的变量。
{}
void Init()
{
//创建网络套接字
_sockfd = socket(AF_INET,SOCK_DGRAM,0); //创建我们的网络套接字这里以UDP为主
if(_sockfd < 0)//判断
{
perror("socket fail!!!");
exit(1);
}
printf("socket套接字创建成功 sockfd:%d\\n",_sockfd);
//将我们的网络套接字进行bind
//是我们要填充我们的sockaddr_in结构体,我们是网络通信的所以要创建我们的sockaddr_in
struct sockaddr_in addr;
//我们在填充这个结构体之前我们最好将我们这个结构体进行清空——方式垃圾数据污染导致我们程序崩溃,防止二次调试。
bzero(&addr,sizeof(addr));
//填充我是一个网络的通信协议,便于我们后续的绑定
addr.sin_family=AF_INET;//声明我是一个网络协议
//我们在绑定前要进行设置我们的IP和端口号,便于我们与我们该文件进行帮绑定
addr.sin_port = htons(_port);//绑定我们的端口号,将我们的端口号设置为我们的大端。
addr.sin_addr.s_addr = inet_addr(_ip.c_str());//我们的网络是点分十进制的,我们的要转成4字节的
int ret = bind(_sockfd,(struct sockaddr*)&addr, sizeof(addr));//我们将我们的字节序和我们的网络文件进行绑定
if(ret < 0)
{
perror("bind fail!!!");
exit(2);
}
printf("绑定成功");
}
void start()
{
_running = true;//表示我的程序开始进行运行
while(_running)
{
char buff[MAX_IF_SIZE];//创建我们的缓冲区表示我们将读到的数据储存在我们的buff中
struct sockaddr_in src_addr;//创建我们的网络通信的结构体,我们在收到数据的时候我们要进行填写
socklen_t addrrlen=sizeof(src_addr);//设置我们的长度
//收消息
//src_addr 输出型参数表示这个是谁给我发的消息,他的信息都在这个结构体当中
//addrlen 当输出型参数表示一个结构体多大,当输入型参数的时候表示信息量的大小
ssize_t ret = recvfrom(_sockfd,buff,sizeof(buff)-1,0,(struct sockaddr* )&src_addr,&addrrlen);
if(ret < 0)//判断
{
perror("收消息 fail!!!");
exit(3);
}
buff[ret] = 0;//将最后一个位置设置为\\0
std::cout<<buff<<std::endl;
//发消息
//我们可以后续写一些的任务进行发送
std::string echo_string = "serve echo@";
echo_string += buff;
// memset(buff,0,sizeof(buff));
// std::cin>>buff;
ret = sendto(_sockfd,echo_string.c_str(), echo_string.size(),0,(struct sockaddr* )&src_addr,addrrlen);
}
}
~serve(){}
private:
int _sockfd; //文件描述符
uint16_t _port; //端口号
std::string _ip; //ip协议
bool _running; //程序是否运行
};
serve.cc
#include "serve.hpp"
int main(int argc ,char* argv[])
{
if(argc != 3) //我们要求我们要输入我们的./serve ip prot
{
perror("input fail!!!, 你应该输入:./serve ip prot");
exit(-1);
}
printf("服务器正在运行\\n");
uint16_t port = std::stoi(argv[2]);//端口号要转为对应的类型传入,不能以字符串进行传入
serve* se = new serve(argv[1],port); //创建一个服务端的变量
se->Init(); //调用初始化
se->start(); //调用开始
return 0;
}
client.hpp
#include"commit.h"
//编写客户端
const int fail_fd = -1; //文件描述符初始化
class client
{
public:
client(std::string ip,uint16_t port) //初始化
: _sockfd(fail_fd)
,_ip(ip)
,_port(port)
{}
void Init()
{
//创建网络套接字
_sockfd = socket(AF_INET,SOCK_DGRAM, 0);
if(_sockfd<0)
{
perror("socket fail!");
exit(1);
}
//我们的客户端不需要显示进行bind,如果我们进行了bind我们的客户端到了后面我们是要进行输入我们的端口号的,如果我们果断的进行了bind我的端口号就不能进行唯一性了。
//我们的client要进行bind,我们在首次发送的时候不我们的os会自动的给我们的client进行绑定,os知道ip,端口号,采用随机的方式进行端口号的绑定
//我们为什么不手动的进行bind?因为我们的一个端口号只能被一个端口号进行bind为了避免我们的端口号进行冲突。
//client端口号是几不重要,我们只要是唯一就可以了
//为什么我们的服务端要进行bind因为我的服务端只可能有一个我们的客户端可以有很多个。所以我们要知道我们的服务端的端口号便于我们后面的使用
}
void Start()
{
struct sockaddr_in src_add;
socklen_t addrlen=sizeof(src_add);
bzero(&src_add,addrlen);
src_add.sin_family=AF_INET;
src_add.sin_port=htons(_port);
src_add.sin_addr.s_addr=inet_addr(_ip.c_str());
while(1)
{
std::string buff;
std::cout<<"Plase Enter# ";
std::getline(std::cin,buff);
int n = sendto(_sockfd,buff.c_str(),buff.size(),0 ,(struct sockaddr*)&src_add, sizeof(src_add));
if(n<0)
{
perror("sendto fail!!!");
exit(-2);
}
//写文件
char buf[MAX_IF_SIZE];
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
ssize_t ret = recvfrom(_sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&addr,&len);
if(ret<0)
{
perror("recvfrom fail!!!");
exit(-3);
}
buf[ret] =0 ;
std::cout<<buf<<std::endl;
}
}
~client(){}
private:
int _sockfd;
uint16_t _port;
std::string _ip;
};
client.cc
#include "client.hpp"
int main(int argc ,char* argv[])
{
if(argc<3)
{
perror("input fail!!!, 你应该输入:./client ip prot");
exit(-1);
}
printf("客户端启动\\n");
uint16_t prot = std::stoi(argv[2]);
client* cl = new client(argv[1],prot);
cl->Init();
cl->Start();
return 0;
}
commit.h
#pragma once
#include<iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <cstdio>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include <strings.h>
#define MAX_IF_SIZE 1024
makefile
.PHONY:all
all: client serve
client:client.cc
g++ -o $@ $^
serve:serve.cc
g++ -o $@ $^
.PHONY:clean
clean:
rm -rf client serve
效果的展示
这是我们的三个ip我们时使用我们分别进行测试。

我们的内网ip
我们的服务端的回答还没有书写,我们下篇文章优化的时候会写的。

图1
我们发现我们服务端是连不上的。

图2

图3
图4
我们能得到下面的三种结论:
1.bind我们的公网ip(111.229.245.253)——fail(图2)
2.binding我们的回环网络(127.0.0.1)和我们的内网ip(10.0.0.13)——success(图1,图3)
3.一个绑定我们的binding我们的回环网络(127.0.0.1)另一个绑定我们的我们的内网ip(10.0.0.13)——fail(图4)
我们的公网ip并没有配备到我们的ip上,这样一来我们的公网ip无法的直接进行bind!我们的公网ip大多配备到我们对应的公司服务器中。
如果我们进行bind的时候那么我们到未来就必须的使用serve进行我们地址的绑定,我们亦可以不进行bind这样我们就能收到所有的人给我发送过来的信息了。
这里我们这样改。
我们的serve.cc
#include "serve.hpp"
int main(int argc ,char* argv[])
{
if(argc != 2) //我们要求我们要输入我们的./serve ip prot
{
perror("input fail!!!, 你应该输入:./serve prot");
exit(-1);
}
printf("服务器正在运行\\n");
uint16_t port = std::stoi(argv[1]);//端口号要转为对应的类型传入,不能以字符串进行传入
//serve* se = new serve(argv[1],port); //创建一个服务端的变量
serve* se = new serve(port);
se->Init(); //调用初始化
se->start(); //调用开始
return 0;
}
我们的serve.hpp
#include "commit.h"
const int fail_fd = -1; //我们文件描述符的初始化
class serve {
public:
// serve(const std::string& ip,uint16_t port) //构造函数我们这需要我们的ip和我们的端口号用来表示我们的唯一性
// :_sockfd(fail_fd),_ip(ip),_port(port), _running(false) //我们要初始化我们对应的变量。
// {}
serve(uint16_t port) //构造函数我们这需要我们的ip和我们的端口号用来表示我们的唯一性
:_sockfd(fail_fd),_port(port), _running(false) //我们要初始化我们对应的变量。
{}
void Init()
{
//创建网络套接字
_sockfd = socket(AF_INET,SOCK_DGRAM,0); //创建我们的网络套接字这里以UDP为主
if(_sockfd < 0)//判断
{
perror("socket fail!!!");
exit(1);
}
printf("socket套接字创建成功 sockfd:%d\\n",_sockfd);
//将我们的网络套接字进行bind
//是我们要填充我们的sockaddr_in结构体,我们是网络通信的所以要创建我们的sockaddr_in
struct sockaddr_in addr;
//我们在填充这个结构体之前我们最好将我们这个结构体进行清空——方式垃圾数据污染导致我们程序崩溃,防止二次调试。
bzero(&addr,sizeof(addr));
//填充我是一个网络的通信协议,便于我们后续的绑定
addr.sin_family=AF_INET;//声明我是一个网络协议
//我们在绑定前要进行设置我们的IP和端口号,便于我们与我们该文件进行帮绑定
addr.sin_port = htons(_port);//绑定我们的端口号,将我们的端口号设置为我们的大端。
//addr.sin_addr.s_addr = inet_addr(_ip.c_str());//我们的网络是点分十进制的,我们的要转成4字节的
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(_sockfd,(struct sockaddr*)&addr, sizeof(addr));//我们将我们的字节序和我们的网络文件进行绑定
if(ret < 0)
{
perror("bind fail!!!");
exit(2);
}
printf("绑定成功");
}
void start()
{
_running = true;//表示我的程序开始进行运行
while(_running)
{
char buff[1024];//创建我们的缓冲区表示我们将读到的数据储存在我们的buff中
struct sockaddr_in src_addr;//创建我们的网络通信的结构体,我们在收到数据的时候我们要进行填写
socklen_t addrrlen=sizeof(src_addr);//设置我们的长度
//收消息
//src_addr 输出型参数表示这个是谁给我发的消息,他的信息都在这个结构体当中
//addrlen 当输出型参数表示一个结构体多大,当输入型参数的时候表示信息量的大小
ssize_t ret = recvfrom(_sockfd,buff,sizeof(buff)-1,0,(struct sockaddr* )&src_addr,&addrrlen);
if(ret < 0)//判断
{
perror("收消息 fail!!!");
exit(3);
}
buff[ret] = 0;//将最后一个位置设置为\\0
std::cout<<buff<<std::endl;
//发消息
//我们可以后续写一些的任务进行发送
std::string echo_string = "serve echo@";
if(buff!=nullptr)
echo_string += buff;
// memset(buff,0,sizeof(buff));
// std::cin>>buff;
ret = sendto(_sockfd,echo_string.c_str(), echo_string.size(),0,(struct sockaddr* )&src_addr,addrrlen);
}
}
~serve(){}
private:
int _sockfd; //文件描述符
uint16_t _port; //端口号
//std::string _ip; //ip协议
bool _running; //程序是否运行
};
主要就是改了![]()
我们测试一 下:

注意我们不要忘记我们在我们对应的服务器也要进行我们对应端口的开放。

这样设置以后只要是我们的本地ip都能进行我们对应的使用了。

当然我们还有一些关于这里的一些优化,我们写篇文章再见




