首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Linux】: Socket 编程

【Linux】: Socket 编程

作者头像
IsLand1314
发布2025-01-09 11:09:42
发布2025-01-09 11:09:42
63800
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

1. 理解源 IP 地址和目的 IP 地址

  • IP 在网络中,用来标识主机的唯一性
  • 注意:后面我们会讲 IP的分类,后面会详细阐述IP的特点但是这里要思考一个问题:数据传输到主机是目的吗?不是的。因为数据是给人用的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览?

但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的 qq,迅雷,浏览器。 而启动的 qq,迅雷,浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于就拿到了数据。

  • 所以:数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程才是目的。

但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?这就要在网络的背景下,在系统中,标识主机的唯一性。

在进行网络通信的时候,是不是我们的两台机器在进行通信呢?

  • 网络协议中的下三层,主要解决的是,数据安全可靠的送到远端机器
  • 用户使用应用层软件,完成数据发送和接收的
  • 先把这个软件启动起来-->进程

日常网络通信的本质:就是进程间通信!!(从应用层上看,不用看网络栈的底层传输)

要进程间通信,就要先把进程标识出来

2. 认识端口号

定义

  • 端口号是传输层协议的一部分。

特点

  • 端口号是一个 2 字节(16 位)的整数
  • 用于标识一个进程,告诉操作系统当前的数据应交给哪个进程处理
  • IP 地址 + 端口号 可以唯一标识网络上某台主机的某个进程
  • 一个端口号只能被一个进程占用

端口号 port: 无论对于 client 和 server,都能唯一的标识该主机上的一个网络应用层的进程

端口号范围划分0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.

  • 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的

3. 理解端口号和进程 ID

在公网上:

  • IP 地址能标识唯一的一台主机,端口号,能表示主机上唯一的一个进程
  • IP:port == 标识全网唯一的一个进程

这种 IP+port 的模式,就叫做 socket (插口,插座) ,实现了客户端和服务器的唯二进程进行通信

例如:10086 是一个 IP,工号 12345 的员工就是 port 端口号

  • 进程 ID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程,但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这样做

💡 端口号 vs pid

pid 已经能标识一台主机上的进程唯一性了,为什么还要搞一个端口号??

  • 不是所有的进程都要网络通信,但是所有进程都要有 pid
  • 为什么不直接用 Pid 做端口?将系统和网络部分解耦合,给网络部分设计了单独的规则,防止系统修改进程 pid 造成混乱

就像一个人有身份证号还有工号,不能用工号代替身份证号,是不同场景下的,如果这样那你从公司离职了呢~

下三层通信示意图

我们的客户端,如何知道服务器的端口号是多少?

每一个服务的端口号必须是众所周知的,精心设计,被客户端知晓的

注意:端口号和进程ID都可以唯一表示一个进程, 但是一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定

源端口号和目的端口号:

传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号 简单来说就是 “数据是哪个发的, 最后要发给谁

4. 传输层协议 -- TCP / UDP 协议

TCP(Transmission Control Protocol 传输控制协议):

  • TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议
  • TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。
  • 其次,TCP协议是保证可靠的协议(也意味着要做更多的事情),数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法

UDP(User Datagram Protocol 用户数据报协议):

  • UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议
  • 使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。
  • 就像发邮件一样,邮件发出去了并不管

⭕ 传输层同时存在 tcp 和 udp 是为什么呢? TCP协议和UDP协议不存在哪个更好的说法? 可靠和不可靠都是中性词,就像化学中的惰性一样

  • TCP可靠 意味着 在设计和维护上更复杂,不可靠就会相对做的事少一些更简单。
  • 所以这两个协议在各自特定的场景下,发光发热

TCP:银行,支付...(在网络联通的情况下,丢包可找回)

UDP:信息派发,例如直播...

5. 网络字节序

🧀 网络字节序(Network Byte Order),也称为网络字节顺序,是协议中规定好的一种数据表示格式。它用于在计算机网络中进行数据通信时,统一数据的字节顺序,确保数据在不同主机之间传输时能够被正确解释。

🔥 我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,

  • 大小端:没有好坏之分,就是一种不同的选择(来源于格列夫游记里面对吃鸡蛋先吃大小端的辩论)
  • 定义:小端:低权值位放在低地址处(小小小)。大端反之

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

由于我们不能保证通信双方存储数据的方式是一样的,因此网络中传输的数据必须考虑大小端问题。

TCP/IP 协议规定如下

  • 网络数据流采用大端字节序,即低地址高字节

无论主机是大端机还是小端机,都必须按照 TCP/IP 协议规定的网络字节序来发送和接收数据:

发送端:

  1. 如果发送端是小端机,需要先将数据转换成大端字节序,然后再发送到网络中。
  2. 如果发送端是大端机,则可以直接发送数据。

接收端:

  1. 如果接收端是小端机,需要先将接收到的数据转换成小端字节序,然后再进行数据识别。
  2. 如果接收端是大端机,则可以直接进行数据识别。

总的来说,就是如果当前发送主机是小端, 就需要先将数据转成大端;否则就忽略, 直接发送即可

注意事项

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  • 接收主机把从网络上接收到的字节依次保存在接收缓冲区中,同样按内存地址从低到高的顺序保存
  • 因此,网络数据流的地址应规定为:先发出的数据是低地址,后发出的数据是高地址

网络字节序与主机字节序之间的转换

为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,系统提供了四个函数,可以通过调用以下库函数实现网络字节序和主机字节序之间的转换。

这些函数名很好记:

  • h 表示主机(host)
  • n 表示网络(network)
  • l 表示 32 位长整数
  • s 表示 16 位短整数

函数说明

  • htonl:将 32 位的长整数从主机字节序转换为网络字节序
  • htons:将 16 位的短整数从主机字节序转换为网络字节序
  • ntohl:将 32 位的长整数从网络字节序转换为主机字节序
  • ntohs:将 16 位的短整数从网络字节序转换为主机字节序

6. Socket 编程

6.1 socket 常见API

🌄 Socket API 是一层网络编程接口,抽象了底层的网络协议,定义在 netinet/in.h 中。它适用于多种网络通信方式,如 IPv4、IPv6,以及 UNIX 域套接字(用于本地进程间通信)。通过 Socket API,程序可以实现跨网络的进程间通信(如通过IP地址和端口号进行的网络通信),也可以实现本地的进程间通信。

常见通用API:

代码语言:javascript
代码运行次数:0
运行
复制
// 创建 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);

① (TCP 常见API)
 
// 开始监听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);

② (UDP 常见API)
 
// 函数用于在面向数据报的套接字(如UDP套接字)上发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
// 用于从套接字接收数据的方法,特别是在使用UDP协议进行数据传输时
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
6.2 sockaddr 结构

我们可以看到上面struct sockaddr *addr出现次数挺多的。实际上在网络上通信的时候套接字种类是比较多的,下面是常见的三种:

  1. unix 域间套接字编程--同一个机器内
  2. 原始套接字编程--网络工具
  3. 网络套接字编程--用户间的网络通信

设计者想将网络接口统一抽象化--参数的类型必须是统一的,底层是一种多态的设计

运用场景:

  • 网络套接字:运用于网络跨主机之间通信+本地通信
  • unix域间套接字: 本地通信
  • 我们现在在使用网络编程通信时是应用层调传输层的接口,而原始套接字:跳过传输层访问其他层协议中的有效数据。主要用于抓包,侦测网络情况

我们现在知道套接字种类很多,它们应用的场景也是不一样的。所以未来要完成这三种通信就需要有三套不同接口,但是思想上用的都是套接字的思想。因此接口设计者不想设计三套接口,只想设计一套接口,可以通过不通的参数,解决所有网络或者其他场景下的通信网络。

由于不同的通信方式(跨网络或本地通信)有不同的地址格式,套接字使用不同的结构体来封装地址信息:

  • sockaddr_in:用于跨网络通信(例如通过 IP 和端口号进行通信)。
  • sockaddr_un:用于本地通信(通过文件路径进行通信)。

为了解决这些不同地址格式的兼容性问题,套接字提供了一个通用的地址结构体 sockaddr,用于统一处理不同的地址结构。

sockaddr、sockaddr_in 和 sockaddr_un 的关系

🔥 sockaddr 是一个通用的套接字地址结构,它为所有不同类型的通信方式提供了统一的接口。具体通信时,sockaddr 实际上指向特定的地址结构(如 sockaddr_insockaddr_un),然后通过强制类型转换来区分是哪种通信方式。

这种设计类似于面向对象编程中的“多态”:sockaddr 可以看作一个“父类”,而 sockaddr_in 和 sockaddr_un 是它的“子类”。在程序中,套接字函数接受 sockaddr* 类型的参数,然后根据具体的通信类型进行处理。

sockaddr 结构体

代码语言:javascript
代码运行次数:0
运行
复制
struct sockaddr {
    __SOCKADDR_COMMON(sa_); /* 公共数据:地址家族和长度 */
    char sa_data[14];       /* 地址数据 */
};

sockaddr_in 结构体(IPv4 套接字地址)

代码语言:javascript
代码运行次数:0
运行
复制
struct sockaddr_in {
    __SOCKADDR_COMMON(sin_);
    in_port_t sin_port;      /* 端口号 */
    struct in_addr sin_addr; /* IP地址 */
    unsigned char sin_zero[sizeof(struct sockaddr) - 
                           __SOCKADDR_COMMON_SIZE - 
                           sizeof(in_port_t) - 
                           sizeof(struct in_addr)];
};

sockaddr_un 结构体(Unix域套接字地址)

代码语言:javascript
代码运行次数:0
运行
复制
struct sockaddr_un {
    __SOCKADDR_COMMON(sun_);
    char sun_path[108]; /* 文件路径 */
};
sockaddr 通用结构的意义 -- 通信方式

sockaddr 作为通用结构,它的前16个比特用于存储协议家族(sa_family 字段)。这个字段用来表明使用的是哪种通信方式:

  • AF_INET:IPv4网络通信。
  • AF_INET6:IPv6网络通信。
  • AF_UNIX:本地通信(UNIX 域套接字)。

通过这种设计,Socket API 可以通过统一的函数接口,处理不同类型的地址格式。开发者只需要将具体的地址结构转换为 ​​​​​​​sockaddr,并设置协议家族字段,套接字函数就能识别出应该进行哪种通信。

通用性带来的优势

🍒 Socket API 的这种设计带来了极大的通用性,使得开发者在同一套代码中可以处理不同的协议类型。例如,函数 sendto() 或 recvfrom() 可以接受 sockaddr* 作为参数,无论是处理 IPv4、IPv6 还是 UNIX Domain Socket,代码都不需要做出太大改动。

代码语言:javascript
代码运行次数:0
运行
复制
int sendto(int sockfd, const void *msg, size_t len, int flags,
           const struct sockaddr *dest_addr, socklen_t addrlen);

在这个函数中,dest_addr 是一个通用的 sockaddr*,程序只需根据实际使用的通信方式(如 IPv4 或 IPv6)对其进行强制类型转换即可。

6.3 IPV4 和 IPV6 的地址表示
  • IPV4IPV6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括16位地址类型, 16位端口号和32位IP地址
  • IPV4IPV6 地址类型分别定义为 常数 AF_INET、AF_INET6。这样只要取得某种 ​​​​​​​sockaddr 结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
  • socket API 可以都用 stsockaddr* 类型表示,在使用的时候需要强制转化成sockaddr_in,这样的好处是程序的通用性,可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的 ​​​​​​​sockaddr 结构体指针做为参数
6.4 in_addr 结构
代码语言:javascript
代码运行次数:0
运行
复制
/* Internet address. */
typedef uint32_t in_addr_t;
struct int_addr
{
	in_addr_t s_addr;
};

in_addr 是一个32位的整数,用来表示IPv4的IP地址。通信过程中,IP地址通常是通过字符串格式(如 "192.168.1.1")转换为 in_addr_t 类型的数值来表示。

总的来说

  • 通过 sockaddr 结构体,Socket API 实现了网络通信和本地通信的统一接口
  • 它的设计理念类似于“多态”,即通过一个通用的接口来处理多种类型的地址格式

7. Socket 接口

7.1 创建 Socket 文件描述符

在 TCP 和 UDP 通信中,首先要创建一个 Socket 文件描述符,它本质上是一个网络文件。其函数原型为:

代码语言:javascript
代码运行次数:0
运行
复制
#include <sys/types.h>          
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

功能:打开一个网络通讯端口,返回一个文件描述符,如果失败,返回 -1。
参数:
    domain:协议域,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(Unix域套接字)。
    type:套接字类型,如 SOCK_STREAM(字节流,TCP)、SOCK_DGRAM(数据报,UDP)。
    protocol:协议类别,通常设置为 0,自动推导出对应的协议,如 TCP/UDP。
7.2 绑定 bind 端口号 (服务器)

在服务器端,必须绑定一个 IP 地址和端口号,以便客户端可以与服务器建立通信。bind() 函数用于将套接字与 IP 和端口号绑定:

代码语言:javascript
代码运行次数:0
运行
复制
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

功能:将指定的 IP 和端口号绑定到套接字,使之监听指定地址。
参数:
    socket:套接字文件描述符。
    address:存储地址和端口号的结构体指针。
    address_len:地址结构体的长度。
7.3 开始监听 Socket (TCP 服务器)

在服务器中,调用 listen() 函数使套接字进入监听状态,准备接受连接请求:

代码语言:javascript
代码运行次数:0
运行
复制
int listen(int socket, int backlog);

功能:让服务器套接字进入监听状态,准备接收客户端连接。
参数:
    socket:监听套接字描述符。
    backlog:全连接队列的最大长度,用于处理多个客户端连接请求。
7.4 接收连接请求 (TCP 服务器)

服务器使用 accept() 从连接队列中提取下一个连接请求,并返回新的套接字用于与客户端通信:

代码语言:javascript
代码运行次数:0
运行
复制
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

功能:获取一个已完成的连接请求,并返回新的套接字用于客户端通信。
参数:
    socket:监听套接字。
    address:存储客户端的地址信息。
    address_len:地址结构的长度。
7.5 建立连接 (TCP 客户端)

客户端通过 connect() 向服务器发起连接请求:

代码语言:javascript
代码运行次数:0
运行
复制
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能:TCP 客户端使用该函数建立与服务器的连接。
参数:
    sockfd:用于通信的套接字文件描述符。
    addr:服务器的地址。
    addrlen:地址长度。
7.6 设置套接字选项 (进阶)

通过 setsockopt() 可以设置套接字的各种属性,例如端口重用等高级功能:

代码语言:javascript
代码运行次数:0
运行
复制
#include <sys/types.h>          
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
                      const void *optval, socklen_t optlen);

功能:设置套接字的选项,如端口重用等。
参数:
    sockfd:套接字文件描述符。
    level:选项的层次(如 SOL_SOCKET,IPPROTO_TCP 等)。
    optname:选项名。
    optval:指向设置值的指针。
    optlen:设置值的长度。
7.7 地址转换函数

IP 地址可以以字符串或整数形式存在。常见的地址转换函数包括:

代码语言:javascript
代码运行次数:0
运行
复制
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
7.8 数据传输函数
发送
代码语言:javascript
代码运行次数:0
运行
复制
#include <sys/types.h>
#include <sys/socket.h>

sendto() (UDP)
用于在 UDP 协议下发送数据:
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

send() 用于在 TCP 协议下发送数据
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

功能:发送数据到指定地址。
参数:
    sockfd:套接字文件描述符。
    buf:要发送的数据。
    len:数据长度。
接收
代码语言:javascript
代码运行次数:0
运行
复制
#include <sys/types.h>
#include <sys/socket.h>

recv() 用于接收 TCP 协议下的数据
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

recvfrom() (UDP)接收来自远程主机的数据:
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

功能:接收数据。
参数:
    sockfd:套接字文件描述符。
    buf:存放接收数据的缓冲区。
7.9 实际使用中的注意事项

INADDR_ANY

🔥 在服务器端,INADDR_ANY(0.0.0.0)可以让服务器监听所有可用的网络接口,而不必指定具体的 IP 地址。这种方式提高了代码的可移植性。

代码语言:javascript
代码运行次数:0
运行
复制
local.sin_addr.s_addr = INADDR_ANY;

Listening Socket vs Connected Socket

  1. Listening Socket:服务器使用它来监听连接请求。它在整个服务器生命周期内存在。
  2. Connected Socket:服务器接收连接请求后,用于与客户端通信的套接字。每个客户端有一个独立的连接套接字。

TCP 通信流程

  1. 服务器初始化:调用 socket() 创建套接字,调用 bind() 绑定地址和端口,调用 listen() 进入监听状态。
  2. 客户端连接:客户端通过 socket() 创建套接字,使用 connect() 发起连接请求。
  3. 三次握手:TCP 客户端与服务器通过三次握手建立连接。
  4. 数据传输:连接建立后,双方可以通过 send() 和 recv() 进行数据传输。
  5. 断开连接:通过四次挥手,客户端和服务器断开连接。

TCP vs UDP

  • TCP:可靠的连接,字节流传输,保证数据顺序。
  • UDP:不可靠传输,数据报传输,适用于实时通信。

8. 共勉 🔥

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,后面我就要进行【Socket 套接字编程】的内容实战啦,请持续关注我 !!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 理解源 IP 地址和目的 IP 地址
  • 2. 认识端口号
  • 3. 理解端口号和进程 ID
  • 4. 传输层协议 -- TCP / UDP 协议
  • 5. 网络字节序
  • 6. Socket 编程
    • 6.1 socket 常见API
    • 6.2 sockaddr 结构
      • sockaddr、sockaddr_in 和 sockaddr_un 的关系
      • sockaddr 通用结构的意义 -- 通信方式
      • 通用性带来的优势
    • 6.3 IPV4 和 IPV6 的地址表示
    • 6.4 in_addr 结构
  • 7. Socket 接口
    • 7.1 创建 Socket 文件描述符
    • 7.2 绑定 bind 端口号 (服务器)
    • 7.3 开始监听 Socket (TCP 服务器)
    • 7.4 接收连接请求 (TCP 服务器)
    • 7.5 建立连接 (TCP 客户端)
    • 7.6 设置套接字选项 (进阶)
    • 7.7 地址转换函数
    • 7.8 数据传输函数
      • 发送
      • 接收
    • 7.9 实际使用中的注意事项
  • 8. 共勉 🔥
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档