字节序:
大端字节序( big endian ): 一个整数的高位字节 (23 ~ 31 bit) 存储在内存的低地址处,低位字节(0 ~ 7 bit) 存储在内存的高地址处
小端字节序( little endian) : 一个整数的高位字节 (23 ~ 31 bit) 存储在内存的高地址处,低位字节(0 ~ 7 bit) 存储在内存的低地址处 ( 应该可以抽象地理解成普通数字的顺序 – 高位在前,低位在后 )
Linux 字节序转换函数
#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 地址
socket
网络编程接口中表示 socket
地址的是结构体 sockaddr
, 其定义如下:
#include <bits/socket.h>
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
};
专用 socket 地址
UNIX本地域
UNIX本地域专用 socket 地址结构体
#include <sys/un.h>
struct sockaddr_un{
sa_family_t sin_family; /* 地址族: AF_UNIX */
char sun_path[108]; /* 文件路径名 */
};
TCP/IP 协议族
TCP/IP 协议族有 sockaddr_in
和 sockaddr_in6
两个专用 socket
结构体,分别用于 IPv4
和 IPv6
struct sockaddr_in{
sa_family_t sin_family; /* 地址族:AF_INET */
u_intl6_t sin_port; /* 端口号,要用于网络字节序表示 */
struct in_addr sin_addr;/* IPv4 地址结构体 */
};
struct in_addr{
u_int32_t s_addr; /* IPv4 网络字节序表示 */
};
struct sockaddr_in6{
sa_family_t sin6_family; /* 地址族:AF_INET */
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]; /* IPv6 地址,要用网络字节序表示 */
};
IP 地址转换函数
#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 );
注意:inet_ntoa
函数将用网络字节序整数表示的 IPv4 地址转化为用点分十进制字符粗汉表示的 IPv4地址。但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此 inet_ntoa
是不可重入的
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
char* ip1 = "10.194.71.60";
char* ip2 = "1.2.3.4";
char* szValue1 = inet_ntoa(*(struct in_addr*)ip1); // 强转
char* szValue2 = inet_ntoa(*(struct in_addr*)ip2);
printf("address 1: %s\n", szValue1);
printf("address 2: %s\n", szValue2);
return 0;
}
/*
address 1: 49.46.50.46
address 2: 49.46.50.46
*/
创建 socket
对于 UNIX/Linux 来说,所有东西都是文件,包括 socket
,可读、可写、可控制和可关闭的文件描述符
#include <sys/types.h>
#include <sys/socket.h>
int socket( int domain, int type, int protocol );
- domain: 使用哪个底层协议族。
- PF_INET (Protocol Family of Internet, 用于IPv4)
- PF_INET6 (用于IPv6)
- type: 指定服务类型
- SOCK_STREAM (流服务 --> TCP协议)
- SOCK_UGRAM (数据报)
- SOCK_DGRAM (UDP协议)
- 通常为 0
调用成功返回一个 socket
文件描述符,失败返回 -1 并设置 errno
命名 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
地址的长度
成功返回 0, 失败返回 -1
监听 socket
socket
被命名之后,还不能马上接受客户连接,需使用如下系统调用来创建一个监听队列,以存放待处理的客户连接
#include <sys/socket.h>
int listen( int sockfd, int backlog );
sockfd
参数指定被监听的 socket
。backlog
参数体制内核监听队列的最大长度。如果队列超过该长度,服务器不接受新的客户连接。典型值为 5. listen 成功返回 0, 失败返回 -1
服务器简洁实现
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
static bool stop = false;
/* SIGTERM 信号的处理函数, 触发时结束主程序中的循环 */
static void handle_term ( int sig ) {
stop = true;
}
int main( int argc, char* argv[]){
signal (SIGTERM, handle_term );
if(argc <= 3 ){
printf( "usage: ip_address port_number backlog\n");
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int backlog = atoi( argv[3] );
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
/* 创建一个 IPv4 socket 地址*/
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( sock, backlog );
assert( ret != -1 );
/* 循环等待连接, 直到有 SIGTERM 信号将它中断 */
while( stop == false ){
sleep( 1 );
}
close( sock );
return 0;
}
接受连接
下面的系统调用从 listen
监听队列中接受一个连接:
#include <sys/types.h>
#Include <sys/socket.h>
int accept( int sockfd, struct sockaddr* addr, socklen_t* addrlen);
注意: accept()
知识从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化
发起连接
客户端通过如下系统调用来主动与服务器建立连接
#include <sys/types.h>
#include <sys/socket.h>
int connect( int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen );
sockfd
参数由socket
系统调用返回一个socket
。serv_addr
参数是服务器监听的socket
地址,addrlen
参数则指定这个地址的长度connect
成功时返回 0 。一旦成功建立连接,sockfd
就唯一地标识了这个连接,客户端就可以通过读写sockfd
来与服务器通信。失败返回-1
。
关闭连接
#include <unistd.h>
int close( int fd );
close
系统调用并非总是立即关闭一个连接,而是将 fd
的引用计数 -1
。只有当 fd
计数为 0 时,才真正关闭连接。多进程程序中,一次 fork
系统调用默认将使父进程中打开的 socket
的引用计数加 1 。
可使用
#include <sys/socket.h>
int shutdown( int sockfd, int howto )
进行立即终止连接
数据读写
TCP数据读写
socket
编程接口提供了几个专门用于 socket
数据读写的系统调用,它们增加了对数据读写的控制。其中用于 TCP
流数据读写的系统调用是:
#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);
flag
参数为数据收发提供额外的控制,如 MSG_OOB
为应用程序提供了发送和接受带外数据的方法
server.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#define BUF_SIZE 1024
char* basename( char* fullname );
int main( int argc, char* argv[] ){
if( argc <= 2 ){
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr);
address.sin_port = htons( port );
int sock = socket( AF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( sock, 5 );
assert( ret != -1 );
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
int connfd = accept( sock, (struct sockaddr*)&client, &client_addrlength );
if( connfd < 0 ){
printf( "errno is : %d\n", errno );
}
else{
char buffer[ BUF_SIZE ];
memset( buffer, '\0', BUF_SIZE);
ret = recv( connfd, buffer, BUF_SIZE - 1, 0 );
printf( "got %d bytes of normal data '%s'\n", ret, buffer );
ret = recv( connfd, buffer, BUF_SIZE - 1, MSG_OOB );
printf( "got %d bytes of oob data '%s'\n", ret, buffer );
ret = recv( connfd, buffer, BUF_SIZE - 1, 0);
printf( "GOT %d bytes of normal data '%s'\n", ret, buffer );
close( connfd );
}
close( sock );
return 0;
}
client.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
char* basename( char* fullname );
int main( int argc, char* argv[]){
if( argc <= 2 ){
printf("usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in server_address;
bzero( &server_address, sizeof( server_address ) );
server_address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &server_address.sin_addr );
server_address.sin_port = htons(port);
int sockfd = socket( AF_INET, SOCK_STREAM, 0);
assert( sockfd >= 0 );
if( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( struct sockaddr_in ) ) < 0 ){
printf( "connection errno is : %d\n", errno );
}
else{
const char* oob_data = "abc";
const char* normal_data = "123";
send( sockfd, normal_data, strlen( normal_data ), 0 );
send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
send( sockfd, normal_data, strlen( normal_data ), 0 );
}
close( sockfd );
return 0;
}
UDP 数据读写
socket
编程接口中用于 UDP
数据报读写的系统调用是:
#include <sys/types.h>
#include <sys/socket.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
读取 sockfd
上的数据, buf
和 len
参数分别指定读缓冲区的位置和大小。因为 UDP
通信没有连接的概念,所以每次读取数据都需要获取发送端的 socket
地址,即参数 src_addr
所指的内容, addrlen
参数则指定该地址的长度
sendto
类似
flags
参数以及返回值的含义均与 send/recv
系统调用的 flags
参数及返回值相同
通用数据读写函数
#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
结构体类型地指针,msghdr
结构体定义如下: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; /* 辅助函数中的 flag 参数,并在调用过程中更新 */ };
-
msg_name
成员指向一个socket
地址结构变量。它自会顶通信对方的socket
地址。对于面向连接的TCP
协议,该成员灭有意义,必须被设置为NULL
。这是因为对数据流socket
而言,对方的地址已经知道。 -
msg_namelen
成员则指定了msg_name
所指socket
地址的长度 -
msg_iov
成员是iovec
结构体类型的指针,iovec
结构体的定义如下struct iovec{ void* iov_base; /* 内存起始地址 */ size_t iov_len /* 这块内存的长度 */ };
iovec
结构体封装了一块内存的起始位置和长度。msg_iovlen
指定这样的iovec
结构体对象有多少个。recvmsg
则利用这些信息进行分散读 (scatter read); 对于sendmsg
而言,msg_iovlen
块分散内存中的数据将被一并发送,即集中写 (gather write)
-
地址信息函数
想知道一个连接 socket
的本端 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
对应的本端 socket
地址,并将其存储于 address
参数指定的内存中,该 socket
地址的长度则存储于 address_len
参数指向的变量中。 如果实际 socket
地址的长度大于 address
所指的内存区大小,那么该 socket
地址将被姐u但。 getsockname
成功时返回 0 , 失败返回 -1 并设置 errno。
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 option_len );
level
参数指定要操作哪个协议的选项。
level | option name | 数据类型 | 说明 |
---|---|---|---|
SOL_SOCKET( 通用 socket 选项, 与协议无关) | SO_DEBUG | int | 打开调试信息 |
SO_REUSEADDR | int | 重用本地地址 | |
SO_TYPE | int | 获取 socket 类型 | |
SO_ERROR | int | 获取并清除 sokcet 错误状态 | |
SO_DONTROUTE | int | 不查看路由表,直接将数据发送给本地局域网内的主机 | |
SO_RCVBUF | int | TCP 接受缓冲区大小 | |
SO_SNDBUF | int | TCP 发送缓冲区大小 | |
SO_KEEPALIVE | int | 发送周期性保活报文以维持连接 | |
SO_OOBINLINE | int | 接受到的带外数据将存留在普通数据的输入队列中(在线存留),此时我们不能使用带 MSG_OOB 标志的读操作来读取带外数据 (应该想读取普通数据那样读取带外数据) | |
SO_LINGER | linger | 若有数据待发送,则延迟关闭 | |
SO_RCVLOWAT | int | TCP 接受缓存区低水位标记 | |
SO_SNDLOWAT | int | TCP 发送缓存区低水位标记 | |
SO_RCVTIMEO | timeval | 接受数据超时 ( 11 章 ) | |
SO_SNDTIMEO | timeval | 发送数据超时 ( 11 章 ) | |
IPPROTO_IP(IPv4选项) | IP_TOS | int | 服务类型 |
IP_TTL | int | 存活时间 | |
IPPROTO_IP(IPv6选项) | IPV6_NEXTHOP | sockaddr_in6 | 吓一跳 IP 地址 |
IPV6_RECVPKTINFO | int | 接收分组信息 | |
IPV6_DONTFRAG | int | 禁止分片 | |
IPV6_RECVTCLASS | int | 接收通信类型 | |
IPPROTO_TCP(IPv6选项) | TCP_MAXSEG | int | TCP 最大报文段大小 |
TCP_NODELAY | int | 禁止 Nagle 算法 |
网络信息API
socket
地址的两个要素,即 IP
地址和端口号,都是用数值表示的。不便于记忆和扩展(IPv4 -> IPv6) 。所以常用主机名来访问一台机器,而避免直接使用 IP
地址。同样用服务名称来代替端口号。比如下面两个 telnet
命令具有完全相同的作用:
telnet 127.0.0.1 80
telnet localhost www
上诉例子中, telnet
客户端程序是通过调用某些网络信息 API
来实现主机名到 IP
地址的转换,以及服务名称到端口号的转换的
gethostbyname 和 gethostbyaddr
gethostbyname
根据主机名称获取主机的完整信息,gethostbyaddr
根据 IP
地址获取主机的完整信息。定义如下:
#include <netdb.h>
struct hostent{
char* h_name; /* 主机名 */
char** h_aliases; /* 主机别名列表,可能有多个 */
int h_addrtype; /* 地址类型(地址族) */
int h_length; /* 地址长度 */
char** h_addr_list /* 按网络字节序列出的主机 IP 地址列表 */
};
getservbyname 和 getservbyport
getservbyname
根据名称获取某个服务的完整信息,getservbyport
根据端口号获取某个服务的完整信息
#include <netdb.h>
struct servent* getservbyname( const char* name, const char* proto );
struct servent* getservbyport( int port, const char* proto );
name
指定目标服务的名字port
指定目标服务对应的端口号proto
指定服务类型,传递 “tcp” 表示获取流服务,传递 “udp” 表示获取数据报服务,传递NULL
表示获取所有类型的服务
这两个函数返回的都是 servent
结构体类型的指针,结构体 servent
的定义如下:
#include <netdb.h>
struct servent{
char* s_name; /* 服务名称 */
char** s_aliases; /* 服务的别名列表,可能有多个 */
int s_port; /* 端口号 */
char* s_proto; /* 服务类型,通常是 tcp 或者 udp */
}
getaddrinfo
getaddrinfo
既能通过主机名获得 IP
地址 (内部使用的是 gethostbyname
), 也能通过服务名获得端口号 (内部使用的是 getservbyname
)。该函数定义如下:
int getaddrinfo( const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result );
hostname
接收主机名,也可以接收字符串表示的IP
地址service
接收服务名,也可以接收字符串表示的十进制端口号hints
是应用程序给getaddrinfo
的一个提示,以对getaddrinfo
的输出进行更精确的控制,可以被置为NULL
表似乎允许getaddrinfo
反馈任何可用的记过result
指向一个链表,该链表用于存储getaddrinfo
反馈的结果
getaddrinfo
反馈的每一条结果都是 addrinfo
结构体类型的对象,结构体 addrinfo
的定义如下:
struct addrinfo {
int ai_flags;
int ai_family; /* 地址族 */
int ai_socktype; /* 服务类型, SOCK_STREAM 或 SOCK_DGRAM */
int ai_protocol;
socklen_t ai_addrlen; /* socket 地址 ai_addr 的长度 */
char* ai_canonname; /* 主机的别名 */
struct sockaddr* ai_addr; /* 指向 socket 地址 */
struct addrinfo* ai_next; /* 指向下一个 sockinfo 结构的对象 */
};
getnameinfo
getnameinfo
函数通过 socket
地址同时获得以字符串表示的主机名 ( 内部使用的是 gethostbyaddr 函数 ) 和服务名 ( 内部使用的是 getservbyport 函数 )。定义如下:
#include <netdb.h>
int getnameinfo( const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags );
getnameinfo
将返回的主机名存储在 host
参数指向的缓存中,将服务名存储在 serv
参数指向的缓存中, hostlen
和 servlen
参数分别指定这两块缓存的长度。 flag
参数控制 getnameinfo
的行为。