IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Linux网络编程 | socket介绍、网络字节序与主机字节序概念与两者的转换、TCP/UDP 连接中常用的 socket 接口 -> 正文阅读

[网络协议]Linux网络编程 | socket介绍、网络字节序与主机字节序概念与两者的转换、TCP/UDP 连接中常用的 socket 接口


套接字

什么是套接字?

所谓 套接字 (Socket) ,就是对网络中 不同主机 上的应用进程之间进行双向通信的端点的抽象。

UNIX/Linux下一切皆文件。 socket 就是可读、可写、可控、可关的文件描述符。socket 最开始的含义是一个 (IP地址,端口)(IP, port) 。唯一地表示了使用 TCP 通信的一端。

为什么要用到套接字?

我们知道,数据链路层、网络层、传输层协议是在内核中实现的。而 socket 就是 操作系统 提供给 应用程序 通过 系统调用 访问这些 协议服务 的一组 APIsocket 不但可以访问内核中 TCP/IP 协议栈,而且访问其他网络协议栈。

socket 定义的 API 提供哪些功能?

  1. 应用程序数据用户缓冲区 中复制到 TCP/UDP 内核发送缓冲区 ,将发送数据交付内核。
  2. TCP/UDP 内核接收缓冲区 中复制数据到 用户缓冲区 ,以读取数据。
  3. 帮助 应用程序 修改 内核中各层协议的某些头部信息 或 其他数据结构,从而精确地控制底层通信行为。(如:通过 setsockopt函数 来设置 IP 数据报 在网络上的存活时间)

socket 的主要 API 都定义在 sys/socket.h 头文件中。Linux 提供了一套定义在 netdb.h 头文件中的网络信息 API ,以实现 主机名IP地址 之间的转换,以及服务名称和端口号之间的转换。


socket 地址

存储 socket 的地址信息的数据结构有三种:sockaddrsockaddr_insockaddr_un 。我们将 sockaddr 称为 通用 socket 地址 ,将 后两者 称为 专用 socket 地址 。这样划分的意义在于:在使用时,我们可以选择自己所需要的结构,通信时再将我们所使用的结构强转为 sockaddr ,这样就能保证数据格式的一致。
在这里插入图片描述

通用 socket 地址

sockaddr 的定义如下:

#include<bits/socket.h>
struct sockaddr
{
	sa_family_t 	sa_family; // 地址族类型(sa_family_t)的变量。
	char 			sa_data[14]; // 存放 socket 地址值。
}

地址族类型通常与协议族类型对应:

协议族地址族描述地址值含义和长度
PF_UNIXAF_UNIXUNIX本地协议族文件的路径名,长度可达108字节
PF_INETAF_INETTCP/IPv4协议族16 bit 端口号和 32 bit IPv4 地址,共6字节
PF_INET6AF_INET6TCP/IPv6协议族16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围ID,共26字节

PF_*AF_* 都定义在 bits/socket.h 头文件中,且有完全相同的值,因此二者经常混用。

然而,通用的 sockaddr 对于各个协议族而言适用性并不好—— sa_data目标IP地址端口信息 混在一起了。因此,Linux 为各个协议族提供了专门的 socket 地址结构体。


拓展: 由上表易知,14 字节的 sa_data 无法容纳多数协议族的地址值。因此,Linux 定义了一个新的通用 socket 地址结构体:

#include<bits/socket.h>
struct sockaddr_storage
{
	sa_family_t 		safamily;
	unsigned long int   __ss_align;
	char 				__ss_padding[128-sizeof(__ss_ag=lign)];
}

这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是 __ss_align 成员的作用)。


专用 socket 地址

sockaddr_un 的定义如下:

UNIX 本地域协议族 使用 sockaddr_un 地址结构体:

#include<sys/un.h>
struct sockaddr_un
{
	sa_family_t sin_family; // 地址族:AF_UNIX
	char sun_path[108]; // 文件路径名
}

sockaddr_in 的定义如下:

TCP/IP协议族IPv4 使用 sockaddr_in 地址结构体:

struct sockaddr_in
{
	sa_family_t     sin_family; /* 地址族:AF_INET */
	u_int16_t       sin_port;   /* 端口号,要用网络字节序表示 */
	struct in_addr  sin_addr;   /* IPv4地址结构体 */
	char            sin_zero;   /* 不使用 */
}struct in_addr
{
	u_int32_t 		s_addr; /* 32位 IPv4 地址,要用网络字节序表示 */
}

可以清楚看到,该结构体解决了 sockaddr 的缺陷,把 portaddr 分开储存在两个变量中。sockaddr_insockaddr 长度一样,都是 16 个字节,即 占用的内存大小是一致的 ,因此可以互相转化。二者是并列结构,指向 sockaddr_in 结构的指针也可以指向 sockaddr

sockaddrsockaddr_in 是 Linux网络编程中最常用的 socket 结构体,sockaddr_in 用于 socket 定义和赋值;sockaddr 用于函数参数。 一般先把 sockaddr_in 变量赋值后,强制类型转换后传入 参数为 sockaddr 的函数。

sockaddr_in6 的定义如下:

TCP/IP协议族IPv6 使用 sockaddr_in6 地址结构体:

struct sockaddr_in6
{
	sa_family_t      sin6_family;  	 /* 地址族:AF_INET6 */
	u_int16_t        sin6_port;   	 /* 端口号,要用网络字节序表示 */
	u_int32_t 		 sin6_flowinfo;  /* 流信息,应设置为0 */
	struct in6_addr  sin6_addr;   	 /* IPv6地址结构体 */
	u_int32_t        sin6_scope_id;	 /* scope ID,尚处于实验阶段 */
}struct in6_addr
{
	unsigned char	 sa_addr[16];    /* 128位 IPv6 地址,要用网络字节序表示 */
}

网络字节序与主机字节序

在使用网络协议的编程中,在两台使用不同存储模式(大端/小端)的主机之间传递数据时,往往会产生歧义。解决问题的方法是:

  • 发送端总是把要发送的数据转化成大端字节序数据后再发送
  • 接收端根据自己的字节序决定是否将传送过来的数据进行转换(自身模式为小端则转换,为大端则不转换)

因此将 大端字节序 称为 网络字节序小端字节序 称为 主机字节序

Linux 提供了如下 4 个函数来完成 主机字节序网络字节序 之间的转换:

#include<netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostlong);
unsigned long int ntotl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

/* h代表主机字节序,n代表网络字节序,l代表长整型,s代表短整型 */

上述函数中,长整型函数 通常用来转换 IP地址短整型函数 用来转换 端口号


地址转换

记录日志时,我们习惯用 可读性好的字符串 来表示 IP地址;编程时,我们往往更需要以 整数(二进制数) 形式表示 IP地址。而这种频繁切换的需求需要通过函数来满足:

#include <arpa/inet.h>
in_addr_t inet_addr(const char *strptr); 
// 将点分十进制的 字符串IP地址 转换为网络字节序的 整数IP地址 ,失败时返回INADDR_NONE
char *inet_ntoa(struct in_addr in);
// 将网络字节序的 整数IP地址 转换为点分十进制的 字符串IP地址,该函数内部用一个静态变量存储转化结果
// 函数的返回值指向该静态内存,因此 inet_ntoa 是不可重入的。
int inet_aton(const char *cp, struct in_addr *inp);
// 将点分十进制的 字符串IP地址 转换为网络字节序的 整数IP地址(与addr的区别它会认为如255.255.255.255这类特殊地址有效),成功返回1,失败返回0。
in_addr_t inet_network(const char *cp); 
// 将点分十进制的 字符串IP地址 转换为主机字节序的 整数IP地址
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);

/* 同时适用于 IPv4 和 IPv6: */
int inet_pton(int af, const char* src, void* dst);
// 将 字符串 表示的IP地址src(用点分十进制表示的IPv4地址或用十六进制字符表示的IPv6地址)转换成 网络字节序整数 表示的IP地址
// 转换结果存在dst指向的内存中。af指定地址族(AF_INET 或 AF_INET6),成功返回1、失败返回0并设置errno。
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
// 将 网络字节序的整数IP地址 转换为 点分十进制的字符串IP地址,成功返回目标存储单元的地址、失败返回NULL并设置errno。
// cnt指定目标存储单元的大小,通过下面两个宏来指定大小:
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16 // 用于 IPv4
#define INET6_ADDRSTRLEN 46 // 用于 IPv6 

不可重入的 inet_ntoa 函数

struct in_addr addr1,addr2;
ulong l1,l2;
l1 = inet_addr("1.1.1.1");
l2 = inet_addr("127.0.0.1");
memcpy(&addr1, &l1, 4);
memcpy(&addr2, &l2, 4);
char *cp1 = inet_ntoa(addr1);
char *cp2 = inet_ntoa(addr2);
cout << "address 1: " << cp1 << endl;
cout << "address 2: " << cp2 << endl;

输出结果:
在这里插入图片描述
我们会发现,由于 inet_ntoa 的返回值指向一个函数内部的静态内存,因此最后一次传入的参数会掩盖之前的参数。


TCP/UDP 连接中常用的 socket 接口

1. 创建 socket

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
domain:底层协议族
type:服务类型
/* 服务类型主要有 */
// SOCK_STREAM服务(流服务),对于 TCP/IP 协议族而言,表示传输层使用 TCP 协议。
// SOCK_UGRAM服务(数据报),对于 TCP/IP 协议族而言,表示传输层使用 UDP 协议。
/* 拓展 */
// Linux内核版本2.6.17起,type可以是上述两种类型与下面两个标志的 与值
// SOCK_NONBLOCK 将新创建的 socket 设置为非阻塞的。
// SOCK_CLOEXEC 用 fork 调用创建子进程时在子进程中关闭该 socket
// 2.6.17版本前,文件描述符的这两个属性都需要使用额外的系统调用(如 fcntl)来设置。
protocol:通常是唯一的(前两个参数已经完全确定了它的值),大部分情况下被设为0,表使用默认协议。

返回值:系统调用成功返回一个 socket 文件描述符,失败返回 -1 并设置 errno。

2. 命名/绑定 socket

创建 socket 时,我们给他指定了地址族,但是 并未指定使用该地址族中哪个具体地址 。 将一个 socket 与具体的地址绑定称为给 socket 命名。

  • 我们通常要给服务器命名 socket ,因为只有命名后客户端才知道如何连接服务器。
  • 客户端不需要命名 socket ,它通常使用操作系统自动分配的 socket 地址。
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, sklen_t addrlen);
// 将 my_addr 所指的 socket 地址分配给未命名的 sockfd 文件描述符,addrlen 指出该地址的长度。
// 成功返回0、失败返回-1并设置errno。
// 其中两种常见errno:
// EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问。如:普通用户将 socket 绑定到知名服务端口(端口号0~1023)。
// EADDRINUSE:被绑定的地址正在使用中。如:将 socket 绑定到一个处于 TIME_WAIT 状态的 socket 地址。

3. 监听 socket

socket 被命名之后,还不能立马接受客户连接,需要使用系统调用来创建一个 监听队列 以存放待处理的客户连接(同时有 全连接半连接 ):

#include<sys/socket.h>
int listen(int sockfd, int backlog);
// sockfd 指定被监听的 socket;backlog 提示内核监听队列的最大长度(常设为5),超过 backlog+1 则不受理新的客户连接,客户端也收到 ECONNREFUSED 错误信息。
// Linux内核2.2版本之前,backlog 指的是所有处于 半连接状态(SYN_RCVD)和 完全链接状态(ESTABISHED)的 socket 总上限。
// 2.2版本之后,它中表示处于 完全连接状态 的 socket 的上限。半连接状态 的 socket 上限值由 /proc/sys/ipv4/tcp_max_syn_backlog 内核参数定义。
//  成功返回0;失败返回-1并设置errno

4. 接受连接

accept 可以从 全连接队列 中接受一个客户连接,但 accept 只是从监听队列中取出连接,而不关心连接处于何种状态(ESTABLISHEDCLOSE_WAIT),更不关心任何网络状态的变化(取出的客户端可能掉线了)。

#include <sys/types,h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 将 addr 所指的 socket 地址分配给执行过 listen 系统调用的 socket 文件描述符,地址长度为 addrlen。
// 成功时返回一个新的 连接socket,唯一地标识被接受的这个连接,服务器可通过读写该 socket 与被接受连接对应的客户端通信;失败返回-1并设置errno。

5. 发起连接

服务器 通过 listen 系统调用来 被动 接受连接,客户端 通过 connect 系统调用来 主动 与服务器建立连接。

#include<sys/types.h>
#include<sys/socket.h>
int connetct(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
// sockfd:socket 系统调用返回的文件描述符。
// serv_addr:处于服务器监听队列的客户端 socket 地址
// 成功返回0,sockfd 唯一标识这个链接,客户端可以通过读写 sockfd 与服务器通信;失败返回-1并设置errno。 
其中两种常见的errno是:
- ECONNREFUSED:目标端口不存在,连接被拒绝。服务器发送给客户端一个复位报文段(seq=0),客户端不必回复复位报文段,应关闭连接或重新连接。
- ETIMEDOUT:连接超时。进行若干次重连,每次重连超时时间都增加一倍。

6.断开连接

关闭连接就是关闭连接对应的 socker 。 可以通过 close 系统调用来关闭文件描述符。

#include<unistd.h>
int close(int fd);
// fd:待关闭的socket

值得一提的是, close 并非立即关闭一个链接,而是将 fd 的引用计数减 1 。当 fd 的引用计数为 0 时,才真正关闭连接。多进程程序中,一次 fork 系统调用默认将使父进程中打开的 socket 的引用计数加 1 ,因此必须在 父子进程中都 对该 socket 执行 close 调用才能关闭连接。

如果无论如何都要立刻终止连接,可以使用 shutdown 系统调用:

#include<sys/socket.h> // 从隶属头文件可以看出,它是专门为网络编程设计的
int shutdown(int sockfd, int howto);
// sockfd:待关闭的 socket。
// howto:shutdown 的行为。
// 成功返回0,失败返回-1并设置errno。

howto 可选值有:

可选值含义
SHUT_RD关闭 sockfd 上读的这一半。应用程序不能再执行读操作,并且该 socket 接收缓冲区中的数据都被丢弃。
SHUT_WR关闭 sockfd 上写的这一半。sockfd 的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再执行写操作。这种情况下,连接处于半关闭状态。
SHUT_RDWR同时关闭 sockfd 上的读和写

closeshutdown 最大的不同是,close 关闭连接时只能将 socket 上的读和写同时关闭,而 shutdown 可以分别(或同时)关闭。


7. 数据读写

对文件的读写 writeread 同样适用于 socke ,但 socket 还有专门用于 socket数据读写 的系统调用:

/* 用于TCP流数据读写的系统调用 */
#include<sys/types>
#include<sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// recv 读 sockfd 上的数据,send 往 sockfd 上写数据。
// buf 和 len 分别指定缓冲区的位置和大小。
// flags 为数据收发提供额外控制,通常为0。
// 成功时返回读/写的数据长度,失败返回-1并设置errno。
// recv 成功时返回的长度可能小于 len,因此可能要多次调用 recv 以读取完整数据;recv 返回 0 意味着 通信对方已经关闭连接。

/* UDP数据读写 */
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
// buf 和 len 参数分别指定读/写缓冲区的位置和大小
// 由于 UDP 通信没有连接的概念,所以我们每次读取数据都需要获取发送端的 socket 地址,即 src_addr 所指向的内容。
// dest_addr 指定接收端的 socket 地址。
/* 上述两个系统调用也可以用于面向连接(STREAM)的 socket 的数据读写,只需将最后两个参数都设置为 NULL */

/* 兼容 TCP流数据 和 UDP数据报 的数据读写 */
#include<sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

struct msghdr
{
	void* msg_name;				// socket 地址,对于TCP协议,需设置为NULL。
	socklen_t msg_namelen;		// socket 地址的长度
	struct iovec* msg_iov;		// 分散内存块,详情见下文。
	int msg_iovlen;				// 分散内存块的数量
	void* msg_control;			// 指向辅助数据的起始位置
	socklen_t msg_controllen;	// 辅助数据的大小
	int msg_flags;				// 复制函数中的 flags 参数,recvmsg 还会在调用过程中将某些更新后的标志设置到 msg_flags 中。
}

struct iovec
{
	void *iov_base; // 内存起始地址
	size_t iov_len; // 这块内存长度
}
对于 recvmsg 而言,数据将被读取并存放在 msg_iovlen 块分散的内存中,这种操作被称为 分散读(scatter read)。
对于 sendmsg 而言,分散内存中的数据将被一并发送,这称为 集中写(gather write)。

flags 可选值:

选项名含义sendrecv
MSG_CONFIRM指示数据链路层持续监听对方的回应,直到得到答复。仅能用于 SOCK_DGRAM 和 SOCK_RAW 类型的 socket。YN
MSG_DONTROUTE不查看路由表,直接将数据发送给本地局域网络内的主机。用于发送者确定目标主机就在本网络。YN
MSG_DONTWAIT对 socket 的此次操作将是非阻塞的YY
MSG_MORE告诉内核应用程序还有更多数据要发送,内核将 超时等待 新数据写入 TCP 发送缓冲区后一并发送。防止 TCP 发送过多小报文段,提高传输效率。YN
MSG_WAITALL读操作仅在读取到指定数量的字节后才返回NY
MSG_PEEK窥探读缓存中的数据,本次读操作不会清除这些数据。NY
MSG_OOB发送或接收紧急数据(带外数据)YY
MSG_NOSIGNAL往读端关闭的管道或 socket 连接中读写数据时不引发 SIGPIPE 信号YN

flags 参数只对 sendrecv 的当前调用生效,而后面讲到的 setsockopt 系统调用会永久地修改 socket 某些属性。


7.1 带外数据

实际应用中,通常无法预知 带外数据 何时到来,幸运的是 Linux 内核检测到 TCP 紧急标志(URG)时,将通知应用程序有带外数据需要接受。通知方法有两种:

  • I/O复用 产生的异常事件
  • SIGURG 信号

但应用程序接到通知后也只知道有带外数据要来,解决的时间的不确定性,但仍不知道带外数据在数据流中的具体位置。想要知道具体位置可以通过如下系统调用:

#include<sys/socket.h>
int sockatmark(int fockfd);

sockatmark 判断 sockfd 是否处于带外标记,即下一个被读取到的数据是否是带外数据。若是返回 1 ,此时就可以用带 MSG_OBB 标志的 recv 调用来接收带外数据;若不是返回 0


8. 地址信息函数

我们可以知道一个连接 socket 的 本端socket地址 ,以及 远端socket地址

#include<sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len); // 获取 sockfd 对应的socket地址
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len); // 远端socket地址
// 将获得的地址存于 address 指定的内存中,地址长度存于 address_len 中。
// 如果实际 socket 地址长度大于 address 所指内存区的大小,那么该 socket地址 将被截断。
// 成功返回0,失败返回-1并设置errno。

9. 总结

用一张图总结 TCP三次握手 过程中对 socket接口 的使用。
在这里插入图片描述

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-22 13:50:08  更:2021-08-22 13:52:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 15:05:10-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码