一、套接字概念

在这里插入图片描述

1.1. 什么是 Socket?

  • Socket 直译为“插座”,在编程中是网络通信的端点
  • 在 Linux 中,Socket 被实现为一种特殊类型的“文件”(伪文件),通过文件描述符操作。
  • 读写 socket 和读写文件几乎是一样的接口(如 read()write()close()),这是 UNIX 哲学的体现:一切皆文件

1.2 Socket 与通信协议的关系

  • Socket 并不是协议,它只是对 TCP/IP 协议族的一种封装。
  • 通信本质是通过协议进行的,Socket 只是编程接口(API)
  • 不同语言调用的 Socket 接口可能略有差别,但底层逻辑相同。

1.3 Socket 如何唯一标识一个连接?

  • 一个 socket 由 (IP地址, 端口号) 唯一标识;
  • 两个进程之间的连接由两个 socket 组成:(源IP:源端口, 目标IP:目标端口) → 称为 socket pair
  • socket pair 唯一标识一条 TCP 连接(或 UDP 会话)。
  • 在网络通信中,套接字一定是成对出现的。 一端的发送缓冲区对应对端的接收缓冲区。

在这里插入图片描述

  1. 套接字定义?
    • 套接字是一套网络通信的接口,这个接口中封装了传输层协议(tcp/udp)
    • 接口就是 API,也就是一套函数
  2. socket(插座),套接字通信的组成部分?
    • 服务器端,插座的作用
      • 被动接受连接的角色,不会主动发起连接的
      • 绑定固定 IP 和端口,这样客户端才能进行连接
    • 客户端
      • 主动发起连接的角色,连接服务器
      • 连接服务器需要地址
        • IP + 端口
  3. 怎么用(所有编程语言的通信流程都是固定的)?
    • 服务器有通信流程
    • 客户端有通信流程
  4. 套接字通信过程中数据要使用网络字节序
    • 字节序:字符在内存中存储顺序,单位是字节,char类型不需要研究字节序问题,大于1字节的数据类型,在内存中存储需要研究字节序的问题
      • 网络字节序
      • 主机字节序
    • 字符串没有字节序问题,字符串是字符的集合

二、网络字节序

2.1 概述

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。

字节序类型说明示例(0x12345678)
大端序(Big Endian)高字节存低地址内存排列:12 34 56 78
小端序(Little Endian)低字节存低地址内存排列:78 56 34 12

在这里插入图片描述

  1. 概念
    • 小端(Little-Endian),也称为主机字节序
      • 在内存的低地址位,存储数据的低位字节,在内存的高地址位存储数据的高位字节
      • 低低高高
      • PC机数据都是小端存储,跟CPU架构有关系
    • 大端(Big-Endian),网络字节序
      • 在内存的低地址位,存储数据的高位字节,在内存的高地址位存储数据的低位字节
      • 低高高低
      • 网络通信的时候,要使用大端的字节序
    • 为什么网络传输要使用大端?
      • 这是一个标准,跨主机通信,不同的主机使用的默认字节序不同,按照本地默认字节序发送和解析数据有可能会出现数据混乱的问题,因此要有一个标准
  2. 字节序举例
    char —> 255(10进制)—> ff(16进制)
    • 有一个 16 进制的数,有32位:0xab5c01ff —> 4字节
      • 小端:ff 01 5c ab (内存低地址位—>内存高地址位)
      • 大端:ab 5c 01 ff

2.2 大小端转换函数(API)

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

	#include <arpa/inet.h>
	
	// 主机字节序转换为网络字节序
	// IP是整型数,通常使用的IP地址是字符串
	uint16_t htons(uint16_t hostshort); // 端口
	uint32_t htonl(uint32_t hostlong); // IP地址(ipv4)
	
	// 网络字节序转换为主机字节序
	uint16_t ntohs(uint16_t netshort);
	uint32_t ntohl(uint32_t netlong);
  • h表示host,n表示network,s表示unsigned short,l表示unsigned int(32位长整数),s表示16位短整数。
  • 在网络通信的时候,IP 和 端口都需要使用大端模式(网络字节序)
  • 在本地,默认我们使用的 IP 和端口是小端存储,在通信的时候需要转换
  • IP(IPv4) -> int ->32bit
  • 端口(unsigned short)-> 16bit
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

2.3 IP地址转换函数

早期:

#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);
char *inet_ntoa(struct in_addr in);
  • 只能处理IPv4的ip地址
  • 不可重入函数
  • 注意参数是struct in_addr

现在:

	#include <arpa/inet.h>
	
	// p 代表主机字节序,字符串类型的IP地址
	// 将主机字节序的字符串类型的IP转换为大端的整型数
	int inet_pton(int af, const char *src, void *dst);
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • 支持IPv4和IPv6
  • 可重入函数
  • 其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr。
  • 因此函数接口是void * addrptr。

int inet_pton(int af, const char *src, void *dst);

  • p 代表主机字节序,字符串类型的IP地址
  • 将主机字节序的字符串类型的IP转换为大端的整型数
  • 参数:
    • af:地址族协议
      • AF_INET:使用 IPv4 的网络协议
      • AF_INET6:使用 IPv6 的网络协议
    • src:传入参数,代表主机字节序的字符串类型的IP地址,要被转换的数据
    • dst:传出参数,存储了转换之后的大端的IP地址
  • 返回值:
    • 成功:0
    • 失败:-1

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

  • 将大端的整型数转换为主机字节序字符串类型的IP
  • 参数:
    • af:地址族协议
      • AF_INET:使用 IPv4 的网络协议
      • AF_INET6:使用 IPv6 的网络协议
    • src:传入参数,代表要被转换的数据,指针指向的内存中存储了大端的IP地址(整型数)
    • dst:传出参数,指针指向的内存中存储了主机字节序的字符串类型的IP地址
    • size:用来修饰第三个参数,描述dst指针指向的内存大小
  • 返回值:
    • 成功:指针指向函数第三个参数dst指向的内存
    • 失败:NULL

2.4 套接字函数

  1. sockaddr 数据结构

在这里插入图片描述

struct sockaddr {
    sa_family_t sa_family;     /* address family, AF_xxx */
    char sa_data[14];          /* 14 bytes of protocol address */
};
  • sa_family_t sa_family; 地址族协议,IPv4,IPv6
  • char sa_data[14]; 端口(2字节)+ IP地址(4字节)+ 填充(8字节)
typedef unsigned short uint16_t;
typedef unsigned int   uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
ipv4套接字结构体
struct sockaddr_in {
   sa_family_t    sin_family; /* address family: AF_INET */
   in_port_t      sin_port;   /* port in network byte order */
   struct in_addr sin_addr;   /* internet address */
};

/* Internet address. */
struct in_addr {
   uint32_t       s_addr;     /* address in network byte order */
};
  1. 创建一个套接字(文件描述符),用于通信或者监听都是可以的
#include <arpa/inet.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 参数:
    • domian:
      • AF_INET:使用 IPv4 的网络协议
      • AF_INET6:使用 IPv6 的网络协议
      • AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
    • type:使用什么样的传输层协议
      • SOCK_STREAM:使用流式传输协议
      • SOCK_DGRAM:使用报式传输协议
    • Protocol:默认写 0
      • 流式协议默认使用 TCP
      • 报式协议默认使用 UDP
  • 返回值:
    • 成功:返回一个文件描述符
    • 失败:-1
  1. 将监听的套接字和本地IP和端口进行关联
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

struct sockaddr_in addr; // 初始化
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数:
    • sockfd:用于监听的套接字,通过调用socket函数创建的
    • addr:将本地的IP和端口信息初始化给该结构体,IP和端口使用大端
      • 绑定的时候服务器端一般IP地址使用 INADDR_ANY == 0
        • 0 == 0.0.0.0
        • 表示绑定本机的所有IP地址
        • 一台主机有多个网卡,就有多个IP地址
          • 先识别实际IP然后绑定
    • addrlen:记录第二个参数指针指向的内存大小,sizeof(struct sockaddr)
  • 返回值:
    • 成功:0
    • 失败:-1
  1. 给监听的套接字设置监听,开始监测客户端连接
int listen(int sockfd, int backlog);
  • 参数:
    • sockfd:监听的套接字,设置监听之前需要先绑定
    • backlog:可以同时检测的新的连接的个数,最大值是128,这个最大值在内核中是写死的
      • 如果参数 > 128 也是按照128处理
  • 返回值:
    • 成功:0
    • 失败:-1
  1. 等待并接受客户端的连接
  • 阻塞函数,没有客户端连接就阻塞
  • 监听的文件描述符读缓冲区没有数据就阻塞
  • 监听的文件描述符读缓冲区有数据就解除阻塞,建立连接
  • 连接建立成功,返回一个通信的文件描述符
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数:
    • sockfd:监听的文件描述符
    • addr:传出参数,保存了建立连接的客户端的地址信息(IP+端口)->大端存储
      • 不需要客户端信息,直接传空
    • addrlen:传入参数,传入addr指针指向的内存大小,传出存储了客户端信息的addr内存大小
      • addr参数为NULL,该参数也写NULL即可
  1. 接收数据
  • 套接字通信过程中读写默认是阻塞的,和管道是一样的
  • 读缓冲区中没数据,读阻塞
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
  • 参数:
    • sockfd:通信的文件描述符
      • accept的返回值(服务器端)
      • 通过socket函数创建得到的,通过connect初始化连接(客户端)
    • buf:存储接收的数据,数据来自通信的文件描述符对应的读缓冲区
    • size:buf 对应的内存容量
    • flag:使用默认属性,指定 0 即可
  • 返回值:
    • > 0:读到的字节数
    • = 0:对方已经断开了连接
    • -1:读异常,失败了
  1. 发送数据
  • 写缓冲区中数据写满了,写阻塞
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
  • 参数:
    • fd:通信的文件描述符
      • accept 的返回值(服务器端)
      • 通过 socket 函数创建得到的,通过 connect 初始化连接(客户端)
    • buf:要发送的数据,数据进入到了通信的文件描述符的写缓冲区
      • 写缓冲区的数据是由内核维护管理的,这里边有数据,内核就会进行发送
    • len:发送的数据的实际长度,strlen();
    • flags:使用默认属性,指定为 0 即可
  • 返回值:
    • >0:发送的实际字节数
    • =0:没有发送任何数据
    • -1:发送失败,异常
  1. 客户端用来连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数
    • sockfd:通信的文件描述符,通过sockfd()得到
    • addr:连接的服务器的IP和端口信息,初始化该变量,使用网络字节序(IP+端口)
    • addrlen:参数addr指向的内存大小
  • 返回值
    • 成功:0
    • 失败:-1
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐