当前位置: 首页 > news >正文

【Linux网络编程】Socket编程-Socket理论入门

目录

网络的几种地址格式

IP地址

MAC地址

 Socket理论基础

网络字节序

大端?小端?

字节序转换函数

Socket常见API

socket() 

bind() 

listen() 

accept() 

connect() 

send() 

recv() 

sendto() 

recvfrom() 

write()/read() 

close()

sockaddr数据结构

结构体类型:

结构示意图:

实际用途:


上文我们已经了解了协议的相关知识以及其作用。说到底还是为了使得多台计算机能够通信,但是也有多台计算机的距离因素影响。如果两台计算机就在相邻的两个座位上,那直接拿条数据线通信就完了。但是正是个人计算机的普及,使得天南海北的计算机都有互相通信的需求,那么我们就需要搭建能跨距离的通信网络。

那么随着计算机网络的普及,这一问题也得到了解决。在该领域比较原始的就是Socket编程了(至于说比较原始是因为,很多个人、企业、组织都基于Socket封装了适用于当下环境的更为方便、系统的编程函数,后面我也会试着简单的封装一些函数,来简化我们的代码的)。

在此之前,我们先来了解一下Socket的理论基础吧。


网络的几种地址格式

IP地址

ip地址是设备在网络中的逻辑标识,用于定位和数据转发。目前主要有IPv4和IPv6两种版本:

IPv4:32位的二进制数,通常以点分十进制的形式表示(192.168.44.188)。按地址范围分为:A、B、C、D、E五类,通过子网掩码划分网络号与主机号。这块之后能学到在提,主要是了解IPv4的地址格式:点分十进制。

IPv6:128位地址,采用十六进制冒号表示(如:2001:0db8:85a3::8a2e:0370:7334),主要是为了解决IPv4地址枯竭问题,广泛应用于新一代网络。

MAC地址

首先,我们上文已经提到了协议格式的问题,在以太网帧格式中,有目的地址和源地址,而且也提到了,这个地址是Mac地址。

MAC地址由48位二进制数(6个字节)组成,通常表示为12位十六进制数 [9],格式为XX-XX-XX-XX-XX-XX [8]。 如:00-16-EA-AE-3C-40就是一个MAC地址,其中前3个字节,16进制数00-16-EA代表网络硬件制造商的编号,它由 IEEE (电气与电子工程师协会)分配,而后3个字节,16进制数AE-3C-40代表该制造商所制造的某个网络产品 (如网卡)的系列号。

我们也可以看出,每台机器都会配备有一个叫做网卡的硬件,这个网卡有一个唯一的编号,这是制造厂商规定的,这是全球唯一。那么有了唯一标识我们才方便寻找地址。

我们可以在电脑的网络属性中看到关于本机的一些网络信息。其中物理地址就是我们的MAC地址。目前博主我使用的这台机房的电脑的物理地址是:52-54-00-11-5C-1C。

在底层通信时,ARP寻址和RARP反向寻址时就需要用到这个地址。

ARP协议:根据Ip地址获取Mac地址

RARP协议:根据Mac地址获取对应的Ip地址

  这个东西上文我们其实已经提到了,此处给出ARP/RARP报文的具体格式。

在物理寻址时,我们想给目标机发送消息,但是我们不知道目标机的MAC地址,那么就先将目的地址给隐藏xx-xx-xx-xx-xx-xx,然后填充本机MAC地址:52-24-00-11-5C-1C。我是发送端,那么我虽然不需要MAC地址,但必须要有对方的IP地址我才能通信,所以帧类型为ARP。在ARP报文中硬件类型这就没什么说的了,协议类型决定了传输过程中采用的协议,例如:TCP/UDP协议。硬件地址长度、协议地址长度也分别使用一字节表示即可。操作类型分为ARP请求和应答、RARP请求和应答。发送时使用ARP请求。发送者以太网地址也是本机已知,填充即可。发送者的IP地址采用IPv4:192.168.44.188,也知道目标IP地址。那么我就可以将这个ARP报文携带着发送到网络环境。然后到了网络环境中,经过 一些列操作 找到了目标机器,那么目标机就会将消息携带的内容中:源地址填充到目标地址,将本机地址填充到源地址,将应答消息发送回去。

 Socket理论基础

Socket (套接字)是网络编程的抽象接口,定义了不同主机进程间通信的方式。它屏蔽了底层网络细节,提供统一编程接口。根据通信协议,Socket分为:

流式Socket(SOCK_STREAM):基于TCP协议,提供可靠、有序的字节流传输。

数据报式Socket(SOCK_DGRAM):基于UDP协议,以无连接的方式传输数据报。

那么Socket通信需要知道的内容就是:

源IP地址:发送端的IP地址,轻易已知。

目的IP地址:接收端的IP地址,一般需要提前获知。

在本文上面我们也介绍了IP地址,那么加上定语:源的、目标的。意义也显而易见了。

只知道这两个IP地址确实就能够使两台机器通信了。但是还有一个问题:机器A打开的是QQ,机器B打开的是微信。A用QQ发送的消息,B能用微信接收吗?显然不可以,那么我怎么确定要发给哪个应用程序呢,也就是具体是哪个进程要通信的问题?

其实看到这里能直接联想到的就是进程ID了,这样就能确定是哪个进程了。但是,进程ID属于内核级数据,我们用户级轻而易举就拿PID来操作,显然危险性极大。此时就需要额外引入一个内容来标识这个东西:端口号-Port。这个端口只能唯一的被一个进程占用(不绝对,有专门的端口复用的技术)。

那么,目的IP可以帮助我找到发送对象的居住楼,端口号可以帮助我找到发送对象的门牌号。

我们的Socket编程就是基于这两点来进行通信的。

网络字节序

大端?小端?

在内存的学习中,我们或许接触过大端存储、小端存储的概念。在网络的学习中,也是有着这么一个内容的。不同的计算机系统可能采用不同字节序(大端or小端)。为了避免混淆,网络通信统一采用大端字节序。编程中,需要通过一些转换函数来确保数据的正确性。

其实这里面也有着一定的历史遗留因素在里面:

《豆包大模型生成》 

        在计算机发展早期,IBM、Intel 等大公司在技术实现上缺乏合作,各自按照不同的理念设计系统。例如,Motorola 的 PowerPC 系列 CPU、IBM 的 System/360 系列计算机采用大端序(低地址存放最高有效字节),而 Intel 的 x86 系列、DEC 的 PDP - 11 系列计算机则采用小端序(低地址存放最低有效字节)。这种因厂商技术路线差异形成的两种字节序存储方式,为后续网络通信埋下了兼容性隐患。

        随着技术发展,更多样化的处理器架构涌现,如 SPARC(除 V9 外)、MIPS 等采用大端序,VAX 等采用小端序。不同架构的设备之间通信时,若直接传输数据,因字节序不同会导致数据解析错误(例如,0x12345678 在大端序和小端序系统中存储顺序不同,直接传输会使接收方误解数据)。然而,当时没有强制的工业标准来统一字节序,这种混乱局面持续存在。

        20 世纪 90 年代,网络技术普及,计算机间数据交换变得频繁。为解决不同字节序设备通信时的数据一致性问题,必须确立统一的传输格式。大端序因与人类阅读数字的习惯(从高位到低位)相似,且在早期网络协议设计中被广泛采用,最终被确定为网络字节序(如所有网络协议均采用大端序传输数据)。而各主机若采用不同字节序(如 x86 的小端序),在发送数据前需转换为网络字节序,接收时再转换回本地字节序,这种转换机制延续至今,成为网络通信的基础规则。

大端存储指的是:低地址存高字节;小端存储指的是:低地址存低字节。 

对于0x12345678,如果是大端存储,从地址0x00000000到0x00000003,依次存放:12、34、56、78;但如果是小端存储,那么存放的就正好相反:78、56、34、12。

如果把地址当成一个双字符数组(即一个位置存放两个字符),

那么大端就是:

data:12       34      56      78

pos:  [0        1        2        3]

小端则是:

data:78      56      34     12

pos: [0        1        2        3]

如果能深刻的理解使用数组模拟大整数加减乘除的算法高精度算法,那么这个就很容易看懂了 。

字节序转换函数

头文件:#include <arpa/inet.h>
返回值类型函数名参数类型参数名
uint32_thtonluint32_thostlong
uint16_thtonsuint16_thostshort
uint32_tntohluint32_tnetlong
uint16_tntohsuint16_tnetshort
#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);

函数名解析:

host to network long(主机字节序转网络字节序,32位)

host to network short (主机字节序转网络字节序,16位)

network to host long(网络字节序转主机字节序,32位)

network to host short(网络字节序转主机字节序,16位)

例如:端口号port通常是short类型的,我们就需要使用int netport = htons(port)转成网络字节序的数字。而ip地址需要使用32位,但是通常的IP地址是点分十进制,还需要使用字符串与数字的转换函数,比较麻烦,使用较少。用的比较多的就是htons了。

Socket常见API

socket() 

int socket(int domain, int type, int protocol);    //(Linux)
SOCKET socket(int domain, int type, int protocol); //(Windows)

函数作用:创建一个套接字,确定通信使用的协议族、数据传输方式及协议,为后续网络操作奠定基础。

参数解析

  • domain:协议族(地址族),如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(Unix 域套接字)。
  • type:套接字类型,SOCK_STREAM(TCP,面向连接,可靠传输)、SOCK_DGRAM(UDP,无连接,高效但不可靠)、SOCK_RAW(原始套接字,直接访问底层协议,较少用)。
  • protocol:具体协议,通常设为 0,由系统根据 domain 和 type 自动选择(如 domain=AF_INET 且 type=SOCK_STREAM 时,自动对应 TCP 协议)。

返回值解析

  • 成功:返回套接字描述符(Linux 下为非负整数文件描述符;Windows 下为 SOCKET 类型句柄)。
  • 失败:返回 -1(Linux)或 INVALID_SOCKET(Windows)。

bind() 

int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen);

函数作用:将套接字与本地特定的 IP 地址和端口绑定。服务端通常在监听前调用 bind() 绑定知名端口以提供服务;客户端一般可不调用(连接时系统自动分配),但也可指定地址端口。

参数解析

  • sockfdsocket() 函数返回的套接字描述符。
  • myaddr:指向本地地址和端口的结构体(如 sockaddr_in 用于 IPv4,需设置 sin_familysin_port(网络字节序)、sin_addr)。
  • addrlenmyaddr 结构体的长度,通常为 sizeof(struct sockaddr)

返回值解析

  • 成功:0
  • 失败:-1

listen() 

int listen(int sockfd, int backlog);

函数作用:仅用于 TCP 服务端,将套接字从主动连接状态(默认)转换为被动监听状态,准备接受客户端连接请求。

参数解析

  • sockfdsocket() 函数返回的套接字描述符(需为 TCP 套接字)。
  • backlog:内核为该套接字维护的最大连接队列长度,包括未完成三次握手的连接和已完成握手的连接。多数系统默认值为 20

返回值解析

  • 成功:0
  • 失败:-1

accept() 

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

函数作用:TCP 服务端调用,从已完成连接队列中取出下一个建立成功的客户端连接。若队列为空,函数阻塞(默认行为)。

参数解析

  • sockfd:已调用 listen() 的 TCP 监听套接字描述符。
  • addr:输出参数,用于存放客户端地址(如 sockaddr_in 结构体)。
  • addrlen:输入 / 输出参数,输入时为 addr 缓冲区长度,输出时为实际存储的地址长度。

返回值解析

  • 成功:返回新的套接字描述符(用于与客户端通信,称为已连接套接字)。
  • 失败:-1

connect() 

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

函数作用

  • TCP 客户端:发起三次握手,与服务端建立连接。
  • UDP:非必须调用,但若调用则 “记住” 目的地址和端口,后续可直接用 send()/recv() 替代 sendto()/recvfrom() 收发数据。

参数解析

  • sockfd:客户端套接字描述符(TCP 或 UDP)。
  • serv_addr:指向服务端地址的结构体(如 sockaddr_in,包含服务端 IP 和端口,网络字节序)。
  • addrlenserv_addr 结构体的长度。

返回值解析

  • 成功(TCP):0(完成三次握手,建立连接)。
  • 失败(TCP):-1

send() 

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 函数作用:在 TCP 连接上发送数据,需确保套接字已连接(通过 connect() 或 accept())。
  • 参数解析
    • sockfd:已连接的 TCP 套接字描述符。
    • buf:指向发送数据缓冲区的指针。
    • len:发送数据的长度(字节数)。
    • flags:标志位,一般设为 0(表示正常发送),可选值如 MSG_OOB(发送带外数据)。
  • 返回值解析
    • 成功:返回实际发送的字节数。
    • 失败:-1

recv() 

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 函数作用:在 TCP 连接上接收数据,若缓冲区无数据且对方未关闭连接,函数阻塞(默认行为)。
  • 参数解析
    • sockfd:已连接的 TCP 套接字描述符。
    • buf:指向接收数据缓冲区的指针。
    • len:缓冲区长度(字节数)。
    • flags:标志位,一般设为 0(正常接收),可选值如 MSG_PEEK(窥探数据,不从接收队列移除)。
  • 返回值解析
    • 成功:返回实际接收的字节数(0 表示对方关闭连接)。
    • 失败:-1

sendto() 

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • 参数
    • sockfd:UDP 套接字描述符。
    • buf:指向发送数据缓冲区的指针。
    • len:发送数据的长度(字节数)。
    • flags:标志位,一般设为 0
    • dest_addr:指向目的地址的结构体(如 sockaddr_in,包含目的 IP 和端口,网络字节序)。
    • addrlendest_addr 结构体的长度。
  • 返回值
    • 成功:返回实际发送的字节数。
    • 失败:-1
  • 作用:向指定地址(dest_addr)的 UDP 套接字发送数据,无需提前建立连接。

 

recvfrom() 

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • 参数
    • sockfd:UDP 套接字描述符。
    • buf:指向接收数据缓冲区的指针。
    • len:缓冲区长度(字节数)。
    • flags:标志位,一般设为 0
    • src_addr:输出参数,用于存放发送方地址(如 sockaddr_in 结构体)。
    • addrlen:输入 / 输出参数,输入时为 src_addr 缓冲区长度,输出时为实际存储的地址长度。
  • 返回值
    • 成功:返回实际接收的字节数。
    • 失败:-1
  • 作用:从 UDP 套接字接收数据,并获取发送方地址。若缓冲区无数据,函数阻塞(默认行为)。

 

write()/read() 

write();//--写数据(与文件写一致)read();//--读数据(与文件读一致)

close()

int close(int fd);//(Linux)
int closesocket(SOCKET s);//(Windows)
  • 参数
    • s(Windows)/ fd(Linux):待关闭的套接字描述符。
  • 返回值
    • 成功:0
    • 失败:-1
  • 作用:关闭套接字,释放相关资源(如文件描述符、缓冲区等)。Windows 下使用 closesocket(),Linux 下使用 close()(本质是关闭文件描述符)。

sockaddr数据结构

结构体类型:

#include <sys/socket.h>  
struct sockaddr {  sa_family_t sa_family;   // 地址族(Address Family),用于指定地址类型  char sa_data[14];        // 存储实际地址数据,其格式和长度随地址族变化  
};  

网络编程中,sockaddr_in 是专门用于表示 IPv4 地址的结构体,定义在头文件 <netinet/in.h> 中。它通过清晰的字段划分,方便开发者操作 IPv4 地址的关键信息(如端口、IP 地址),并与通用结构体 sockaddr 兼容(通过强制类型转换)。

#include <netinet/in.h>  struct in_addr {  in_addr_t s_addr;  // 32 位 IPv4 地址(网络字节序,大端)  
};  struct sockaddr_in {  sa_family_t    sin_family;  // 地址族(必须为 AF_INET,表示 IPv4)  in_port_t      sin_port;    // 端口号(网络字节序,大端)  struct in_addr sin_addr;    // IPv4 地址结构体(含 32 位地址)  char           sin_zero[8]; // 填充字段(无实际意义,用于与 sockaddr 对齐)  
};  

结构示意图:

实际用途:

在bind()函数中需要创建一个sockaddr_in结构体:

struct sockaddr_in serv_addr;  // IPv4 专用结构体  
memset(&serv_addr, 0, sizeof(serv_addr));  // 初始化所有字段为 0  // 设置 IPv4 地址族  
serv_addr.sin_family = AF_INET;  // 设置端口(8080 转换为网络字节序)  
serv_addr.sin_port = htons(8080);  // 绑定所有可用接口(INADDR_ANY)  
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 强制转换为 sockaddr* 类型,传递给 bind 函数  
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));  

感谢大家!

相关文章:

  • 深入了解linux系统—— 基础IO(上)
  • Redis学习打卡-Day3-分布式ID生成策略、分布式锁
  • 基于First Order Motion与TTS的AI虚拟主播系统全流程实现教程
  • UI-TARS本地部署
  • 中级网络工程师知识点7
  • 学习黑客Active Directory 入门指南(一)
  • 先说爱的人为什么先离开
  • Manus 全面开放注册,OpenAI 发布 Codex,ChatGPT 上线 GPT-4.1!| AI Weekly 5.12-18
  • 2KW压缩机驱动参考设计【SCH篇】
  • 【AI】Ubuntu 22.04 4060Ti16G 基于SWIFT框架的LoRA微调 模型Qwen3-1.8B 数据集弱智吧 微调笔记
  • 【DAY22】 复习日
  • Markdown 简历生成器——ResumeCraft 开发历程分享
  • 微信小程序 地图 使用 射线法 判断目标点是否在多边形内部(可用于判断当前位置是否在某个区域内部)
  • Linux概述:从内核到开源生态
  • Python实例题:Flask开发轻博客
  • 异常日志规范
  • UniRef100 ID 转换 UniProtKB ID
  • Qt音视频开发过程中一个疑难杂症的解决方法/ffmpeg中采集本地音频设备无法触发超时回调
  • 【AWS入门】Amazon Bedrock简介
  • 机器学习-人与机器生数据的区分模型测试 - 模型融合与检验
  • 被央视曝光“废旧厂区沦为垃圾山”,江西萍乡成立调查组查处
  • 交响4K修复版《神女》昨晚上演,观众听到了阮玲玉的声音
  • CBA官方对孙铭徽罚款3万、广厦投资人楼明停赛2场罚款5万
  • “先增聘再离任”又添一例,景顺长城基金经理鲍无可官宣辞职
  • 东部沿海大省浙江,为何盯上内河航运?
  • 流失79载,国宝文物“子弹库帛书”(二、三卷)回归祖国