socket地址API
字节序
- 现代CPU的累加器一次都能装载(至少)4个字节(32位机器),即一个整数。
这4个字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题。 - 字节序分为大端字节序和小端字节序。
- 大端字节序是指整数的高位字节存储在内存的低地址处,低位字节存储在内存的高位地址处。
- 小段字节序是指整数的高位字节存储在内存的高地址处,低位字节存储在内存的低位地址处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdio.h> void byteorder() { union { short value; char union_bytes[ sizeof(short) ]; } test; test.value = 0x0102; if (test.union_bytes[0] == 1 && test.union_bytes[1] == 2) { printf("big endian"); } else if (test.union_bytes[0] == 2 && test.union_bytes[1] == 1) { printf("little endian"); } else { printf("unknown"); } } |
主机字节序
- 现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
网络字节序
- 当格式化的数据(32位整型数和16位短整型数)在两台使用不同字节序的主机之间直接传递时,接收端必然错误得解释。
- 解决这个问题的方法是:
发送端总是把要发送的数据转化成大端字节序数据之后再发送,而接收端知道对方发送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端转换,大端就不转换)。 - 因此大端字节序也被称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。
- 另外,即使同一台机器上的两个进程通信,也要考虑字节序的问题。
字节序API
1 2 3 4 5 6 |
#include <netinet/in.h> unsigned long int htonl(unsigned long int hostlong); unsigned short int htons(unsigned short int hostshort); unsigned long int ntohl(unsigned long int netlong); unsigned short int ntohs(unsigned short int netshort); |
通用socket地址
1 2 3 4 5 6 7 |
#include <bits/socket.h> struct sockaddr { sa_family_t sa_family; char sa_data[14]; }; |
- sa_family成员是地址族(sa_family_t)类型的变量,地址族类型通常与协议族类型对应。如下图:
PF_*
与AF_*
都定义在bits/socket.h
头文件中,且两者有完全相同的值,一般可以混用。
- sa_data成员用于存放socket地址值。
- 但是,由上图可见,sa_data根本无法完全容纳多数协议族的地址值。所以Linux定义了下面的新的socket地址结构体:
这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
1 2 3 4 5 6 7 8 |
#include <bits/socket.h> struct sockaddr_storage { sa_family_t sa_family; unsigned long int __ss_align; char __ss_padding[128-sizeof(__ss_align)]; }; |
专用socket地址
- 上面两个通用socket地址不是很好用,比如设置与获取IP地址和端口号就需要执行繁琐的位操作。所以Linux为各个协议族提供了专门的socket地址结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; // 地址族 char sun_path][108]; // 文件路径名 }; // ipv4 struct in_addr { u_int32_t s_addr; // ipv4地址,要用网络字节序表示 }; struct sockaddr_in { sa_family_t sin_family; // 地址族 u_int16_t sin_port; // 端口号,要用网络字节序表示 struct in_addr sin_addr; // ipv4结构体 }; // ipv6 struct in6_addr { unsigned char sa_addr[16]; // ipv6地址,要用网络字节序表示 }; struct sockaddr_in16 { sa_family_t sin6_family; // 地址族 u_int16_t sin6_port; // 端口号,要用网络字节序表示 u_int32_t sin6_flowinfo; // 流信息,应该设置为0 struct in6_addr sin6_addr; // ipv6结构体 u_int32_t sin6_scope_id; // scopeid }; |
IP地址转换API
- 将点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址直接的转换:
1 2 3 4 5 |
#include <arpa/inet.h> in_addr_t inet_addr(const char* strptr); int inet_aton(const char* cp, struct in_addr* inp); char* inet_ntoa(struct in_addr in); |
- 同样的功能,并且使用ipv4和ipv6
1 2 3 4 |
#include <arpa/inet.h> int inet_pton(int af, const char* src, void* dst); const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt); |
socket基础API
创建socket
1 2 3 4 |
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); |
- domain
- PF_INET
- PF_INET6
- PF_UNIX
- type
- SOCK_STREAM
- SOCK_UGRAM
- 对TCP/IP协议族而言,SOCK_STREAM表示传输层使用TCP协议,SOCK_UGRAM表示传输层使用UDP协议。
- Linux内核2.6.17开始,可以使用与SOCK_NONBLOCK和SOCK_CLOEXEC相与的结果,分别表示将新创建的socket设置非阻塞的,以及用fock调用创建子进程时在子进程中关闭该socket。
- 几乎在所有情况下,都应该把这个参数设置为0,表示使用默认协议,因为前两个参数已经决定了协议的值。
- socket系统调用成功时返回一个socket文件描述符,失败返回-1并设置errno。
命名socket
- 创建socket时,我们给它指定了协议族,但是并没有指定使用该地址族中的哪个具体socket地址。
将一个socket与socket地址绑定,称为给socket命名。
1 2 3 4 |
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen); |
- bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen是该socket地址的长度。
- bind成功时返回0,失败则返回-1并设置errno。
- 常见的errno:
- EACCES
被绑定的地址是受保护的地址,仅超级用户能够访问。 - EADDRINUSE
被绑定的地址正在使用中。
- EACCES
监听socket
1 2 3 |
#include <sys/socket.h> int listen(int sockfd, int backlog); |
- socket被命名后,还不能马上接收客户连接,因为还需要listen系统调用创建一个监听队列以存放待处理的客户连接。
- sockfd指定被监听的socket。backlog参数提示内核监听队列的最大长度。
监听队列的长度如果超过baklog,服务器将不受理新的客户连接,客户端也将收到ECONNEREFUSED错误信息。 - 关于这个backlog,在内核2.2之前的Linux中,是指所有处于半连接状态和完全连接状态的socket上限。但是自内核2.2之后,它只表示完全处于连接状态的socket的上限,而处于半连接状态的socket的上限,则由
/proc/sys/net/ipv4/tcp_max_syn_backlog
内核参数定义。
接收连接
1 2 3 4 |
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen); |
- sockfd是执行过listen系统调用的监听socket。
- addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。
- accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接。
服务器可通过读写该socket来与被接受连接对应的客户端通信。
accept失败时返回-1并设置errno。 - 另需要注意的是,accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。
发起连接
1 2 3 4 |
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen); |
- 如果说服务器是通过listen调用来被动接受连接,那么客户端则需要通过connect系统调用来主动与服务器建立连接。
- sockfd是由socket系统调用返回一个socket。
- serv_addr参数是服务器监听的socket地址。addrlen则是对应这个地址的长度。
- connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过这个sockfd来与服务器通信。
connect失败则返回-1并设置errno。- ECONNREFUSED:目标端口不存在。
- ETIMEDOUT:连接超时。
关闭连接
1 2 3 |
#include <unistd.h> int close(int fd); |
- 关闭一个连接实际上就是关闭该连接对应的socket。
- fd是待关闭的socket。
- 不过,close系统调用并不总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0的时候,才真正关闭连接。
- 多进程程序中,一次fork系统调用默认将父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
- 如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用shutdown系统调用(相对于close来说,它是专门为网络编程设计的)。
1 2 3 |
#include <sys/socket.h> int shutdown(int sockfd, int howto); |
- sockfd是待关闭的socket,howto参数决定了shutdown的行为。
TCP数据读写
- 对于文件的读写操作read和write通用适用于socket。
但是socket网络编程提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。 - recv读取sockfd上的数据,send往sockfd上写入数据。flags参数选项如下图:
1 2 3 4 5 |
#include <sys/types.h> #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); |
UDP数据读写
- socket编程接口用于UDP数据报读写的系统调用是recvfrom和sendto。
- recvfrom读取sockfd上的数据,sendto往sockfd上写入数据。
1 2 3 4 5 6 7 |
#include <sys/types.h> #include <sys/socket.h> int recvform(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen); int sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen); |
通用数据读写
- 这些接口不仅能用于TCP数据流,也能UDP数据报。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <sys/socket.h> struct iovec { void* iov_base; // 内存起始地址 size_t iov_len; // 这块内存的长度 }; struct msghdr { void* msg_name; // socket地址 socklen_t msg_namelen; // socket地址的长度 struct iovec* msg_iov; // 分散的内存块 int msg_iovlen; // 分散内存块的数量 void* msg_control; // 指向辅助数据的起始位置 socklen_t msg_controllen; // 辅助数据的大小 int msg_flags; // 复制函数中的flags参数,并在调用过程中更新 }; ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags); ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags); |
带外标记
- 在实际应用中,通常无法预料带外数据何时到来。
但是Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。 - 内核通知应用程序带外数据到达的两种常见的方法如下:
- I/O复用产生的异常事件
- SIGURG信号
- 另外,应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一点则可以通过sockatmark来获取。
1 2 3 |
#include <sys/socket.h> int sockatmark(int sockfd); |
- sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark返回1,是的话,就可以用带MSG_OOB标志的recv调用来接收带外数据。不是的话,sockatmark返回0。
地址信息API
1 2 3 4 |
#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len); int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len); |
- getsockname获取sockfd对应的本端socket地址,getpeername获取sockfd对应的远端socket地址。
socket选项
1 2 3 4 5 |
#include <sys/socket.h> int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict_option_len); int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len); |
- 如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法,那么getsockopt和setsockopt则是专门用来读取和设置socket文件描述符熟悉的方法。
SO_REUSEADDR
- 强制使用被处于TIME_WAIT状态的连接占用的socket地址。
- 经过setsockopt的设置之后,即使sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。
- 此外,也可以通过修改内核参数
/proc/sys/net/ipv4/tcp_tw_recycle
来快速回收被关闭的socket,从而使得TCP连接根本就不进入TIME_WAIT状态,进而允许应用程序立即重用本地的socket地址。
SO_RCVBUF和SO_SNDBUF
- SO_RCVBUF和SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小。
- 不过,当我们使用setsockopt设置了TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。
- TCP接收缓冲区的最小值是256字节,而发送缓冲区的最小值是2048个字节(不过,不同系统可能有不同的默认值)。
- 系统这样做的目的,主要是确保一个TCP连接拥有足够的空闲缓冲区离开处理拥塞。
- 此外,我们还可以直接修改内核参数
/proc/sys/net/ipv4/tcp_rmem
和/proc/sys/net/ipv4/tcp_wmem
来强制TCP接收缓冲区和发送缓冲区的大小没有最小值限制。
SO_RCVLOWAT和SO_SNDLOWT
- 这两个选项分别表示了TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用来判断socket是否可读或可写。
- 当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据。
- 当TCP发送缓冲区中的空闲空间(就是可以写入数据的空间)大于其低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写入数据。
- 默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。
SO_LINGER
- 这个选项用于控制close系统调用在关闭TCP连接时的行为。
- 默认情况下,当我们使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责将该socket对应的TCP发送缓冲区中残留的数据发送给对方。
- 设置这个选项的值时,需要传过去一个结构体。
1 2 3 4 5 6 7 |
#include <sys/socket.h> struct linger { int l_onoff; // 开启(非0)还是关闭(0)该选项 int l_linger; // 滞留时间 }; |
- 这个结构体的值的不同,可能让close系统调用产生3种行为:
- l_onoff等于0。此时的SO_LINGER选项不起作用,close用默认行为关闭socket。
- l_onoff不等于0,l_linger等于0。此时close系统调用立即返回,TCP模块将丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时发送给对方一个复位报文段。
- l_onoff不等于0,l_linger大于0。此时close的行为取决于两个条件:
一是被关闭的socket对应的TCP发送缓冲区中是否还有残留的数据;
二是该socket是阻塞的还是非阻塞的。对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对方的确认。
如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。
如果socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数据是否已经发送完毕。
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Linux_ 命令大全 网络通讯03/16
- ♥ Socket:发送结构化消息-结构体10/19
- ♥ Linux 线程概述&&创建03/31
- ♥ Make&&Makefile03/23
- ♥ Shell 语法记述 第三篇09/05
- ♥ vim编辑器的配置03/18