1、socket
地址API
1.1 主机字节序 和 网络字节序
主机字节序:
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
大端字节序和小端字节序会产生一个问题,当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然会进行错误的解释。解决办法是:发送端总把要发送的数据转换成大端字节序,再发送,而接收端总采用大端字节序接收,并根据需要转换成自己使用的字节序。
因此,大端字节序也称为网络字节序。
Linux提供了4个函数来完成主机字节序 和 网络字节序之间的转换。
#include<netinet/in.h>
unsigned long int htonl( unsigned long int hostlong ); ——"host to networt long"
unsigned short int htons( unsigned short int hostshort ); ——"host to network short"
unsigned long int ntohl( unsigned long int netlong ); ——"network to host long"
unsigned short int ntohs( unsigned short int netshort ); ——"network to host short"
在windows下用C++需要添加下面的头文件:
#include <winsock2.h>
#pragma comment(lib,“ws2_32.lib”)
看函数名就能知道功能,如htonl
,即将长整型的主机字节序转化为网络字节序数据。这四个函数中,长整型通常用来转换IP地址,短整型函数用来转换端口号。当然,它们的功能远远不止于此,任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序。
1.2 通用socket
地址
socket
网络编程接口中表示socket
地址的是结构体sockaddr
,其定义如下:
#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
地址值。但是,不同的协议族的地址具有不同的含义和长度,如下表所示:
根据上表可知,14字节的sa_data
根本无法容纳多数协议族的地址值。因此,Linux定义了下面新的通用的socket
地质结构体:
#include<bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __sspadding[128-sizeof(__ss_align)];
}
新的结构体不仅提供了足够大的空间存放地址,而且是内存对齐的(__ss_align
成员的作用)。
理解socket
地址
假设小明同学想给女神打电话需要知道对方的电话号码,而我们进行网络通信也要知道对方的socket
地址。 电话号码以北京地区为例,如010-82512345,有两部分组成,3位区号,8位号码,3-8组合。区号用于标示这在北京,号码用于标示北京具体哪个电话机。
而在网络通信中,采用类似方法标示socket
地址。 socket地址最关键的两部分为:(ip,port),即IP地址和 port 端口号,比如一个网络地址为192.168.130.55:8000
, 里面的IP地址用于区分计算机,而端口号8000用于区分具体是哪一个socket
。
1.3 专用socket
地址
1.4 IP地址转换函数
#include<arpa/inet.h>
in_addr_t inet_addr( const char* strptr );
int inet_aton( const char* cp, struct in_addr* int );
char* inet_ntoa( struct in_addr in );
net_addr
函数把点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。他失败时返回INADDR_NONE
inet_aton
函数的功能和inet_addr
一样,但是将转化结果存储于参数inp
指向的结构体中。他成功转化返回1,失败返回0。inet_ntoa
函数把网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。但是要注意,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa
是不可重入的。下面的代码揭示了其不可重入性:
char* p1 = inet_ntoa( "1.2.3.4" );
char* p2 = inet_ntop( "10.194.71.60" );
printf("address 1: %s\n", p1);
printf("address 2: %s\n", p2);
输出:
address 1: 10.194.71.60
address 2: 10.194.71.60
2、创建socket
(文件描述符)
说是创建一个socket
,但创建出来的其实只是一个socket
文件描述符,连端口都还没有确定,他携带的信息仅有:该socket
文件描述符使用哪种协议(TCP、UDP等)。
domain
告诉系统使用哪个协议族。对TCP/IP协议族而言,其值设置为PF_INET
或PF_INET6
;对于UNIX本地与协议族而言,设置为PF_UNIX
。type
参数指定服务类型。服务类型主要有SOCK_STREAM
(流服务) 和SOCK_UGRAM
(数据报服务)。分别适用于TCP/IP
协议族 和UDP
协议。
protoaol
是在前两个参数构成的集合下,再选一个具体的协议。几乎在所有情况下,都应该把它设置为0,表示使用默认协议。
socket
系统调用成功时返回一个socket
文件描述符,失败则返回-1并设置errno
。
3、命名socket
(bind
)
有了 socket
地址 和 socket
文件描述符还做不成任何事情,需要把他俩绑定在一起,这也就是bind
的工作:
- 在服务器程序中,通常要给
socket
命名,因为只有命名后客户端才能知道该如何连接他。 - 客户端通常不需要命名
socket
,而是采用匿名方式,即,用操作系统自动分配的socket
地址。
命名socket
的系统调用是bind
,其定义如下:
#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
地址的长度。
4、(服务器)监听socket
(listen
)(被动接受连接)
socket
被命名之后,还不能马上接受客户连接,需要使用如下系统调用创建一个监听队列以存放待处理的客户端连接:
#include<sys/socket.h>
int listen(int sockfd, int backlog);
sockfd
参数指定被监听的socket
。(socket
地址 和socket
文件描述符已经被bind
在一起了,其中socket
文件描述符是主体,他俩绑定在一起后,我们只用文件描述符来指示某个具体的socket
)backlog
参数限定 监听队列中处于完全连接状态(ESTABLISHED
)的连接的最大数量。也就是说,监听队列中,最多只能有backlog
个连接完成了三报文握手过程(也就是ESTABLISHED
状态),其他连接只能停留在SYN_RECV
状态。(这两个状态的详情请参考这篇博客的3.4小节)不过,在实际实验中,处于完全连接状态的最大连接数一般是backlog+1
。listen
成功时返回0,失败返回-1,并设置errno
。
调用过listen
之后,传入listen
的socket
文件描述符自动变成了一个 监听socket
。他就代表着该socket
的监听队列。这一性质,将在下一小节:接受连接中使用。
5、(服务器)接受连接(accept
)
只有接受了某个客户端的连接,并获取到新的socket
,服务器才可通过读写该socket
来与被接受连接对应的客户端通信。accept
系统调用正是用来解决这个问题,他从listen
产生的监听队列中接受一个连接:
#include<sys/socket.h>
#include<sys/types.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
sockfd
参数是执行过listen
系统调用的监听socket
。addr
参数用来获取被接受连接的远端socket
地址。addrlen
参数指出远端socket
地址的长度。
accept
成功时返回一个新的socket
,该socket
唯一标识了被接受的连接。服务器可通过读写该socket
与被接受连接对应的客户端通信。
accept
失败时返回-1并设置errno
。
accept
对客户端的实时情况毫不知情
accept
只是自以为是的从监听队列中取出连接,而不论连接出于何种状态,更不关心网络状况如何变化。
什么??不懂?好,我给你细细道来:
我们不是用accept
从监听队列里拿出来一个连接吗?只要监听队列里面有ESTABLISHED
的连接,accept
就不管三七二十一,返回一个socket
,说:“拿去吧!你可以用这个socket
和客户端通信了。”
这哪行啊?玩意这时候客户端那边断网了呢?怎么可能和他通信?
6、(客户端)发起连接
客户端通过下面的系统调用主动与服务器建立连接:
#include<sys/socket.h>
#include<sys/types.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
sockfd
是socket
系统调用返回文件描述符serv_addr
是使用服务器的IP地址和端口号创建的 “服务器” 的socket
地址socklen_t
是socket
地址的长度connect
成功返回0,失败返回-1并设置errno
。两种常见的errno
如下:
一旦执行了这个函数之后,sockfd
就唯一标识了该连接,客户端通过读写该sockfd
与服务器通信。
7、关闭连接
关闭普通文件描述符的系统调用如下:
#include<unistd.h>
int close(int fd);
8、数据读写
8.1 TCP数据读写
用于TCP读写的系统调用:
#icnlude<sys/socket.h>
#include<sys/types.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
:
recv
读取sockfd
上的数据buf
指针指向存储读入数据的起始地址len
指定读入数据的大小flags
参数见后文recv
成功时,返回实际读取到的数据的长度,他可能小于期望的长度len
。因此可能要多次调用recv
,才能读取到完整的数据。recv
可能返回0,意味着对方已经关闭连接了。recv
出错时返回-1,并设置errno
。
对于send
:
send
往sockfd
上写入数据buf
指针指向被写数据的起始地址len
指定要写的数据的大小send
成功时饭hi实际写入数据的长度,失败返回-1并设置errno
。
8.2 UDP数据读写
用于UDP数据报读写的系统调用是:
#include<sys/socketh.>
#include<sys/types.h>
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);
对于recvfrom
:
- 前四个参数都和TCP的
recv
函数相同 src_addr
参数是指向发送端的socket
地址的指针addrlen
参数指出发送端socket
地址的长度
对于sendto
:
- 前四个参数都和TCP的
send
函数相同 src_addr
参数是指向接收端的socket
地址的指针addrlen
参数指出接收端socket
地址的长度
这两个系统调用的返回值含义与recv
和send
的返回值含义相同。
由于UDP提供非连接服务,所以每次读取和发送都要获取对方的socket
地址,及其长度。
8.3 通用数据读写函数
socket
编程接口提供了一堆通用的数据读写系统调用,不仅适用于TCP,也能用于UDP:
#include<sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
sockfd
参数指定被操作的目标socket
msg
参数是msghdr
结构体类型的指针,其定义如下:- ——
msg_name
是指向socket
地址的指针,它指定对方的socket
地址。对于面向连接的TCP协议,该成员无意义必须设置为NULL
- ——
msg_namenlen
指定了msg_name
地址所指的socket
地址的长度 - ——
msg_iov
成员是iovec
结构体类型的指针(其实就是个结构体数组,msg_iov
是指向其首元素的指针),iovec
结构体定义如下
iovec
结构体封装了一块内存的起始地址和长度。 - ——
msg_iovlen
指定了这样的iovec
结构体对象有多少个。(结构体数组的长度) -
- 对于
recvmsg
而言,数据将被读取并存放在msg_iovlen
块分散的内存中,这些内存的位置和长度由msg_iov
指向的iovec
数组指定,这称为分散读
- 对于
-
- 对于
sendmsg
而言,msg_iovlen
块分散内存中的数据将被一并发送,这称为集中写
- 对于
9、带外标记
#include<sys/socket.h>
int sockatmark(int sockfd);
10、地址信息函数
某些情况下,想获取一个连接的本端socket
地址 和 远端socket
地址,通过下面的系统调用来获取:
#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
对应的本端IP地址,并把它存储到address
指向的socket
地址中,该socket
地址的长度存储于address_len
指向的变量中。如果实际socket
地址的长度大于address
所指内存区的大小,那么该socket
地址将被截断。getsockname
成功返回0,失败返回-1,并设置errno
。getpeername
获取sockfd
对应的远端IP地址,参数及返回值含义与getsockname
相同。
11、socket
选项
下面两个系统调用专门用于获取和设置 socket
文件描述符属性:
#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 opiton_len);
sockfd
参数指定被操作的目标socket
.level
参数指定要操作哪个协议的属性(或选项),比如IPv4、IPv6、TCP等。option_name
参数指定属性的名字。下表中列举了socket
通信中几个比较常用的socket
属性。option_value
和option_len
参数分别是被操作选项的值和长度。不同选项具有不同类型的值,如上表中数据类型
一列所示。
setsockopt
把socket
的相应属性设置为option_value
的值
getsockopt
获取socket
的相应属性,并将其值写入option_value
中
11.1 SO_REUSEADDR
选项
当然,SO_REUSEADDR
的使用情景远不止于此(详细用法可以参考《UNIX网络编程》P179)。
现在先记住SO_REUSEADDR
的作用是:可以重用已经处于连接状态的端口。
11.2 SO_RCVBUF
和 SO_SNDBUF
选项
SO_RCVBUF
和 SO_SNDBUF
分别表示TCP接收缓冲区 和 发送缓冲区的大小。不过,当调用setsockopt
设置TCP的接收缓冲区大小时,系统会将其值加倍(或者其他变大方法),总之一般都会比我们传入的值大,并且不小于最小值。
这样做的目的,主要是确保一个TCP连接有足够的空闲缓冲区处理拥塞(比如快速重传算法就期望TCP接收缓冲区能至少容纳4个大小为SMSS的TCP报文段)。
11.3 SO_RCVlOWAT
和 SO_SNDLOWAT
选项
11.4 SO_LINGER
选项
#include<sys/socket.h>
struct linger
{
int l_onoff; 值非0表示开启,值为0表示关闭 该选项
int l_linger; 滞留时间
};
12、网络信息 API
12.1 什么是 网络信息API?
通过一个例子来讲。我们用用主机名访问一台机器,从而避免直接使用其IP地址。同样,我们用服务名称代替端口号。比如下面两条命令完全等价:
telnet 127.0.0.1 80
telnet localhost www
上面的例子中,telnet
客户端程序是通过调用某些网络信息API来实现主机名到IP地址的转换,以及服务名到端口号的转换的。
下面列举几个较为重要的网络信息API。
12.2 gethostbyname
和 gethostbyaddr
#include<netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
注:这两个版本的函数是不可重入的
#include<netdb.h>
struct hostent
{
char* h_name; 主机名
char** h_aliases; 主机名列表,可能有多个主机名
int h_addrtype; 地址类型,(地址族)
int h_length; 地址长度
char** h_addr_list; 按网络字节序 列出的主机IP地址列表
};
12.3 getservbuname
和 getservbyport
#include<netdb.h>
struct servent* getservbuname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
注:这两个版本的函数是不可重入的
#include<netdb.h>
struct servent
{
char* s_name; 服务名称
char** s_aliases; 服务的别名列表,可能有多个别名
int s_port; 端口号
char* s_proto; 服务类型,通常是tcp或udp
};
12.4 getaddrinfo
getaddrinfo
函数既能通过主机名获得IP地址(内部调用gethostbyname
函数),也能通过服务名获得端口号(内部调用getservbyname
函数)。她是否可重入取决于其内部调用的gethostbyname
getservbyname
是否是他们的可重入版本,该函数定义如下:
#include<netnetdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
hostname
参数可以接收主机名,也可以接收字符串表示的IP地址。service
参数可以接收服务名,也可以接收字符串表示的十进制端口号。hints
参数是应用程序给getaddrinfo
的一个提示,用来对其行为进行更精准的控制。hints
可以被设置为NULL,表示允许函数返回任何可用结果。result
参数指向一个链表,该链表存储了该函数的返回结果。链表的每一个元素都是一个addrinfo
结构体,其定义如下:
struct addrinfo
{
int ai_flags; 其值可取下表中标志的按位异或
int ai_family; 地址族
int ai_socktype; 服务类型,SOCK_STREAM 或 SOCK_DGRAM
int ai_protocol; 具体的网络协议,与socket系统调用中第三个参数相同,通常被置为0
socklen_t ai_addrlen; socket地址 ai_addr 的长度
char* ai_canonname; 主机的别名
struct sockaddr* ai_addr; 指针,指向存储了socket地址的结构体
struct addrinfo* ai_next; 指针,指向链表中下一个 sockinfo 结构体
};
当想要使用hints
参数时,可以设置其aiflags
、ai_family
、ai_socktype
和 ai_protocol
四个字段,其他字段必须被设置为NULL。
下面的代码利用hints
参数获取主机ernest_laptop
上的daytime
流服务信息:
12.5 getnameinfo
getnameinfo
函数可以通过socket
地址获得以字符串表示的主机名和服务名(内部分别使用gethostbyaddr
和 gethostbyport
函数)。她是否可重入取决于其内部调用的gethostbyaddr
和 gtehostbyport
是否是他们的可重入版本。该函数定义如下:
#include<netdb.h>
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
共7个参数:
- 前两个参数分别是我们想要获取其信息的
socket
地址及其长度 - 第3、4、5、6个参数用于存储函数返回的结果:
getnameinfo
将其返回的主机名存储在host
参数指向的缓存中,将服务名存储在serv
指向的缓存中,hostlen
和servlen
参数分别指定这两块缓存的长度。 - 第7个参数:
flags
用于控制getnameinfo
的行为,他可以接收下表中的选项: