0%

Linux网络编程基础API


Linux网络API:

  • socket地址API。一个ip地址和端口对(ip, port)。唯一表示使用TCP通信的一端
  • socket基础API。头文件<sys/socket.h>,包括创建socket、命名socket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项。
  • 网络信息API。Linux提供的网络信息API,实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。头文件<netdb.h>中。

1.socket地址API

主机字节序和网络字节序

  • 大端字节序:一个整数的高位字节(23~31 bit)存储在内存的低地址处,低位字节(0~7bit)存储在内存的高地址处
  • 小端字节序:整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处

现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换:

1
2
3
4
5
6
#include<netinet/in.h> 
// host to network long 将长整型(32bit)的主机字节序数据转化为网络字节序数据
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);

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

通用socket地址

结构体sockaddr,定义如下:

1
2
3
4
5
#include<bits/socket.h> 
struct sockaddr {
sa_family_t sa_family; // 地址族类型,与协议族类型相对应
char sa_data[14]; // 存放socket地址值
}

常见协议族与对应的地址族的关系:

协议族 地址族 描述
PF_UNIX AF_UNIX UNIX本地域协议族
PF_INET AF_INET TCP/IPv4协议族
PF_INET6 AF_INET6 TCP/IPv6协议族

PF_*AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。

不同的协议族的地址值具有不同的含义和长度。14字节的sa_data根本无法完全容纳多数协议族的地址值。因此,Linux定义了下面这个新的通用socket地址结构体:

1
2
3
4
5
6
#include<bits/socket.h>
struct sockaddr_storage {
sa_family_t sa_family;
unsigned long int__ss_align;
char__ss_padding[128-sizeof(__ss_align)];
}

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

专用socket地址

Linux为各个协议族提供了专门的socket地址结构体。

UNIX本地域协议族的专用socket地址sockaddr_un

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

TCP/IP协议族:sockaddr_in(IPV4)和sockaddr_in6(IPV6)地址结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct sockaddr_in {
sa_family_t sin_family;/*地址族:AF_INET*/
u_int16_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_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];/*IPv6地址,要用网络字节序表示*/
};

所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。

IP地址转换函数

IPV4地址:点分十进制字符串
IPV6地址:十六进制字符串
编程中需要将他们转化为整数(二进制数)使用,记录日志则相反,需要由整数转化为可读的字符串。

用于用点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<arpa/inet.h> 

in_addr_t inet_addr(const char*strptr);
// 点分十进制字符串->网络字节序整数,失败返回INADDR_NONE

int inet_aton(const char*cp,struct in_addr*inp);
// 功能与inet_addr相同,结果存在inp指向的地址结构(传出参数),成功返回1,失败返回0

char*inet_ntoa(struct in_addr in);
// 网络字节序整数->点分十进制字符串。函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存
// inet_ntoa是不 可重入的,例如:

char* szValue1 = inet_ntoa(“1.2.3.4”);
char* szValue2 = inet_ntoa(“10.194.71.60”);
printf(“address 1:%s\n”, szValue1);
printf(“address 2:%s\n”, szValue2);
// 返回结果为:
// address1:10.194.71.60
// address2:10.194.71.60

更新函数适用于IPV4和IPV6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<arpa/inet.h>

int inet_pton(int af, const char* src, void* dst);
// 字符串IP地址src->网络字节序整数,结果存储在dst指向的内存中
// 参数af:指定地址族,AF_INET或者AF_INET6
// 成功返回1,失败返回0并设置errno

const char* inet_ntop(int af, const void* src, char* dst,socklen_t cnt);
// 网络字节序整数->字符串IP地址src,前三个参数与之前的函数相同
// 参数cnt:指定目标存储单元的大小,定义两个宏帮助指定大小(分别用于IPV4和IPV6)
// 成功返回目标存储单元的地址,失败返回NULL并设置errno

#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

创建socket

UNIX/Linux系统中:所有东西都是文件。
socket,可读可写、可控制、可关闭的文件描述符。
socket系统调用创建一个socket:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
/*
参数:
domain:底层的协议族。
PF_INET(IPv4)、 PF_INET6(IPv6)、PF_UNIX(本地域)
type:指定服务类型。
SOCK_STREAM(TCP)、SOCK_UGRAM(UDP)、SOCK_NONBLOCK(非阻塞的)、SOCK_CLOEXEC
protocol:选择协议,通常由前两个参数决定。
设置为0,使用默认协议
返回值:
成功返回一个socket文件描述符,失败返回-1并设置errno
*/

命名socket

命名socket:将一个socket与socket地址绑定。
服务器程序中,命名后客户端才知道如何连接它。
命名socket的系统调用:bind函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<sys/types.h>
#include<sys/socket.h>

int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
/*
参数:
sockfd:未命名的sockfd文件描述符
my_addr:指向socket地址
addrlen:socket地址的长度
返回值:
成功返回0,失败返回-1并设置errno。
错误:
EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。
EADDRINUSE,被绑定的地址正在使用中。
*/

监听socket

创建一个监听队列以存放待处理的客户连接,listen函数:

1
2
3
4
5
6
7
8
9
#include<sys/socket.h> 
int listen(int sockfd, int backlog);
/*
参数:
sockfd:指定被监听的socket
backlog:提示内核监听队列的最大长度。如果监听队列的长度超过backlog,服务器不受理新的客户连接,客户端收到ECONNREFUSED错误信息。backlog典型值为5
返回值:
成功返回0,失败返回-1并设置errno。
*/

backlog函数:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 5-3testlisten.cpp
#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>

static bool stop = false;
// SIGTERM信号的处理函数,触发时结束主程序中的循环
static void handle_term( int sig )
{
stop = true;
}

// testlisten函数接收三个参数:IP地址,端口号,backlog值
int main( int argc, char* argv[] )
{
signal( SIGTERM, handle_term );

if( argc <= 3 )
{
printf( "usage: %s ip_address port_number backlog\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1]; // ip地址
int port = atoi( argv[2] ); // 端口号
int backlog = atoi( argv[3] ); // backlog值

int sock = socket( PF_INET, SOCK_STREAM, 0 ); // 创建ipv4 socket文件描述符
assert( sock >= 0 );

struct sockaddr_in address; // 创建一个ipv4 socket地址
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;

inet_pton( AF_INET, ip, &address.sin_addr ); // 字符串ip地址转化为网络字节序整数
address.sin_port = htons( port ); // 主机字节序转化为网络字节序,短整型函数转化端口号

// 命名socket
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );

// 监听socket
ret = listen( sock, backlog );
assert( ret != -1 );

// 循环等待连接,直到有SIGTERM信号将它中断
while ( ! stop )
{
sleep( 1 );
}

close( sock ); // 关闭socket
return 0;
}

服务器程序testlisten,接受3个参数:IP地址,端口号,backlog值。
服务器运行该程序,客户端多次执行telnet命令连接该服务器程序。使用telnet建立连接,执行netstat命令查看服务器上连接的状态。

1
2
3
$./testlisten 192.168.1.109 12345 5#监听12345端口,给backlog传递典型 值5 
$telnet 192.168.1.109 12345#多次执行之
$netstat-nt|grep 12345#多次执行之

在监听队列中,处于ESTABLISHED状态的连接只有6个(backlog值加1),其他的连接都处于SYN_RCVD状态。即完整连接最多有(backlog+1)个。在不同的系统上,运行结果会有些差别,不过监听队列中完整连接的上限通常比backlog值略大。

接受连接

从listen监听队列中接受一个连接,accept函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<sys/types.h> 
#include<sys/socket.h>

int accept(int sockfd, struct sockaddr*addr, socklen_t* addrlen);

/*
参数:
sockfd:执行过listen系统调用的监听socket(处于LISTEN状态的socket,而所有处于ESTABLISHED状态的socket则称为连接socket)
addr:获取被接受的远端socket地址
addrlen:socket地址的长度
返回值:
成功返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信;
失败返回-1并设置errno。
*/

接受一个异常的连接:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 5-5testaccept.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

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]; // ipv4地址
int port = atoi( argv[2] ); // 端口号

struct sockaddr_in address; // 创建socket地址结构体,传入ip地址和端口号
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( PF_INET, SOCK_STREAM, 0 ); // 创建socket文件描述符
assert( sock >= 0 );

// 绑定socket地址结构体与socket文件描述符,命名socket
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );

ret = listen( sock, 5 ); // 监听socket
assert( ret != -1 );

/*暂停20秒以等待客户端连接和相关操作(掉线或者退出)完成*/
sleep(20);

struct sockaddr_in client; // 客户端socket地址结构体
socklen_t client_addrlength = sizeof( client );

// accept接收连接调用,返回值connfd
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
// 接受连接成功,打印客户端地址和端口号
char remote[INET_ADDRSTRLEN ];
printf( "connected with ip: %s and port: %d\n",
inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ), ntohs( client.sin_port ) );
close( connfd );
}

close( sock ); // 关闭socket
return 0;
}

服务器运行testaccept程序,在客户端执行telnet命令连接该服务器的程序:
(服务器ip地址:192.168.1.109)

1
2
$./testaccept 192.168.1.109 54321#监听54321端口
$telnet 192.168.1.109 54321

启动telnet客户端程序,立即断开该客户端的网络连接(建立和断开连接的过程要在服务器启动后20秒内完成)。结果发现accept调用能够正常返回,服务器输出如下:

1
connected with ip:192.168.1.108 and port:38545

服务器运行netstat命令查看accept返回socket连接的状态:

1
2
$netstat-nt|grep 54321
tcp 0 0 192.168.1.109:54321 192.168.1.108:38545 ESTABLISHED

accept调用对于客户端网络断开毫不知情。重新执行上述过程,不过这次不断开客户端网络连接,而是在建立连接后立即退出客户端程序。这次accept调用同样正常返回。服务器运行netstat命令查看:

1
2
$netstat-nt|grep 54321
tcp 1 0 192.168.1.109:54321 192.168.1.108:52070 CLOSE_WAIT

由此可见,accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化。

发起连接

服务器通过listen调用,被动接受连接;客户端通过connect调用,主动与服务器建立连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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:指定地址的长度
返回值:
成功返回0,一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。失败返回-1并设置errno。
常见错误:
ECONNREFUSED:目标端口不存在,连接被拒绝
ETIMEDOUT:连接超时
*/

关闭连接

关闭该连接所对应的socket,通过关闭普通文件描述符的系统调用完成:

1
2
3
4
5
6
#include<unistd.h> 
int close(int fd);
/*
参数fd:待关闭的socket
close函数将fd的引用计数减1,只有当fd的引用计数为0时,才真正关闭连接。
*/

多进程程序中,一次fork系统调用默认使父进程中打开的socket的引用计数加1,因此,必须在父进程和子进程中都对该socket执行close调用,才能将连接关闭。

如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用(相对于close来说,它是专门为网络编程设计的):

1
2
3
4
5
6
7
8
9
10
11
12
#include<sys/socket.h>
int shutdown(int sockfd,int howto);
/*
参数:
sockfd:待关闭的socket
howto:决定shutdown的行为,可选择:
SHUT_RD:关闭sockfd的读,应用程序无法对socket文件描述符执行读操作
SHUT_WR:关闭sockfd的写,应用程序无法对socket文件描述符指向写操作,连接处于半关闭状态
SHUT_RDWD:同时关闭sockfd上的读和写
返回值:
成功时返回0,失败则返回-1并设置errno
*/

shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。

数据读写

TCP数据读写

对文件的读写操作read和write同样适用于socket。用于TCP流数据读写的系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<sys/types.h> 
#include<sys/socket.h>

ssize_t recv(int sockfd, void* buf, size_t len, int flags);
/*
recv作用:读取sockfd上的数据
参数:
sockfd:要读取的sockfd文件描述符
buf:指定读缓冲区的位置(传出参数)
len:缓冲区大小
flags:通常设置为0
返回值:
可能要多次调用recv,才能读取到完整的数据。
返回0表示通信对方已经关闭连接,失败返回-1并设置errno
*/

ssize_t send(int sockfd, const void* buf, size_t len, int flags);
/*
send作用:向sockfd上写入数据
sockfd:要写入数据的sockfd文件描述符
buf:指定读缓冲区的位置(传出参数)
len:缓冲区大小
flags:数据收发的额外控制
*/

发送带外数据:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 5-6oobsend.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
// ip地址与端口号
const char* ip = argv[1];
int port = atoi( argv[2] );

// 创建socket地址结构体
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 );

// 创建socket文件描述符
int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
{
printf( "connection failed\n" );
}
else
{ // 发起连接成功
printf( "send oob data out\n" );
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;
}

接受带外数据:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 5-7oobrecv.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define BUF_SIZE 1024

int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
// 读取ip地址与端口号
const char* ip = argv[1];
int port = atoi( argv[2] );

// 接受地址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 );

// 创建socket文件描述符
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );

// 绑定socket文件描述符与socket地址信息
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );

// socket地址监听
ret = listen( sock, 5 );
assert( ret != -1 );

// 客户端socket地址结构体
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );

// 监听的socket接受连接
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{ // 接受连接成功
char buffer[ BUF_SIZE ]; // 缓冲区

// 读取socket中的数据,写入到buffer缓冲区中,打印缓冲区
memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
printf( "got %d bytes of normal data '%s'\n", ret, buffer );

memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );
printf( "got %d bytes of oob data '%s'\n", ret, buffer );

memset( buffer, '\0', BUF_SIZE );
ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
printf( "got %d bytes of normal data '%s'\n", ret, buffer );

close( connfd ); // 关闭socket
}

close( sock );
return 0;
}

现在服务器上启动5-7oobrecv.cpp服务器程序testoobrecv,客户端执行5-6oobsend.cpp客户端程序testoobsend,向服务器发送带外数据。

1
2
$./testoobrecv 192.168.1.109 54321 #在Kongming20上执行服务器程序,监听 54321端口 $./testoobsend 192.168.1.109 54321 #在ernest-laptop上执行客户端程序 
$sudo tcpdump-ntx-i eth0 port 54321

服务器的输出如下:

1
2
3
got 5 bytes of normal data'123ab' 
got 1 bytes of oob data'c'
got 3 bytes of normal data'123'

客户端发送给服务器的3字节的带外数据“abc”中,仅有最后一个字符“c”被服务器当成真正的带外数据接收。并且,服务器对正常数据的接收将被带外数据截断,即前一部分正常数据“123ab”和后续的正常数据“123”是不能被一个recv调用全部读出的。

UDP数据读写

用于UDP数据报读写的系统调用:

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
#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);
/* recvfrom调用
作用:读取sockfd上的数据
参数:
sockfd:要读取的socket文件描述符
buf:指定缓冲区位置
len:缓冲区的大小
flags:额外控制
src_addr:获取发送端的socket的地址
addrlen:发送端socket地址的大小

*/

ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
/* sendto调用:
作用:向sockfd上写入数据
参数:
sockfd:要写入数据的socket文件描述符
buf:指定缓冲区位置
len:缓冲区的大小
flags:额外控制
dest_addr:指定接收端的socket的地址
addrlen:接收端socket地址的大小
*/

recvfrom/sendto系统调用也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我们已经和对方建立了连接,所以已经知道其socket地址了)。

通用数据读写函数

不仅适用于TCP流数据,也能用于UDP数据报:

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
36
#include<sys/socket.h> 

ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
/* recvmsg调用:
作用:接收读取sockfd上的数据
参数:
sockfd:要读取的socket文件描述符
msg:指向msghdr结构体
flags:额外控制
*/

ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);
/*
作用:向sockfd上写入数据
参数:
sockfd:要写入数据的socket文件描述符
msg:指向msghdr结构体
flags:额外控制
*/

// 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; /*复制函数中的flags参数,并在调用过程中更新*/
};

// iovec结构体定义:
struct iovec {
void* iov_base; /*内存起始地址*/
size_t iov_len; /*这块内存的长度*/
};

msghdr结构体中,msg_name成员指向一个socket地址结构变量。它指定通信对方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。这是因为对数据流socket而言,对方的地址已经知道。

iovec结构体封装了一块内存的起始位置和长度。

分散读(scatter read):recvmsg调用,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定。

集中写(gather write):sendmsg调用,msg_iovlen块分散内存中的数据将被一并发送。

msg_flags成员无须设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。

带外标记

内核通知应用进程带外数据抵达的两种方式:

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

即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。使用sockatmark调用:

1
2
3
4
5
6
7
#include<sys/socket.h>
int sockatmark(int sockfd);
/*
作用:判断sockfd是否处于带外标记,即下一个被读取的数据是否是带外数据
参数fd:需要判断的socket文件描述符
返回值:若是带外数据,返回1,此时可以利用带MSG_OOB标志的recv调用来接收带外数据;若不是,返回0
*/

地址信息函数

获取一个连接socket的本端socket地址,以及远端的socket地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<sys/socket.h> 

int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
/*
作用:获取sockfd对应的本端socket地址,存储在address指向的内存中。如果实际socket地址的长度大于address所指内存区的大小,那么该socket地址将被截断。
参数:
sockfd:需要获取的socket文件描述符
address:指向socket地址结构体的指针,传出参数
address_len:socket地址的长度
返回值:
成功时返回0,失败返回-1并设置errno
*/

int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
/*
作用:获取sockfd对应的远端socket地址
参数,返回值的含义与getsockname调用的相同
*/

socket选项

读取和设置socket文件描述符属性的方法:

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> 

int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
/*
作用:读取sockfd的属性
参数:
sockfd:指定被操作的目标socket文件描述符
level:指定要操作的协议属性(IPV4、IPV6、TCP等)
option_name:指定选项的名字,socket选项
option_value:被操作选项的值
option_len:被操作选项的长度
返回值:
成功返回0,失败时返回-1并设置errno
*/

int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);
/*
作用:设置sockfd的属性
参数,返回值:参照getsockopt调用

*/

image-20220724232946013

对于服务器,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。因为连接socket只能由accept调用返回,而accept从listen监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤,这说明服务器已经往被接受连接上发送出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文段选项。

解决方法:对监听socket设置socket选项,那么accept返回的连接socket将自动继承这些选项。这些socket选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。

对于客户端,socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后,TCP三次握手已完成。

SO_REUSEADDR选项

服务器程序可以通过设置socket选项SO_REUSEADDR,来强制使用被处于TIME_WAIT状态的连接占用的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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 5-9reuse_address.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

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] );

// 创建TCP socket文件描述符
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
// 设置socket属性为SO_REUSEADDR,重用本地地址
int reuse = 1;
setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );

// 创建socket地址并绑定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 );

// 监听socket
ret = listen( sock, 5 );
assert( ret != -1 );

// 客户端接收socket地址,accept调用
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 remote[INET_ADDRSTRLEN ];
printf( "connected with ip: %s and port: %d\n",
inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ), ntohs( client.sin_port ) );
close( connfd ); // 关闭客户端连接
}

close( sock ); // 关闭服务器连接

return 0;
}

SO_RCVBUF和SO_SNDBUF选项

SO_RCVBUF选项:TCP接收缓冲区的大小,最小值256字节
SO_SNDBUF选项:TCP发送缓冲区的大小,最小值2048字节
用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。系统这样做的目的,主要是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞。

修改TCP发送缓冲区的大小:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 5-10set_send_buffer.cpp
#include <sys/socket.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define BUFFER_SIZE 512

int main( int argc, char* argv[] )
{
if( argc <= 3 )
{
printf( "usage: %s ip_address port_number send_bufer_size\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );

// 服务器socket地址
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 );

// 创建socket文件描述符
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );

// 设置TCP发送缓冲区的大小
int sendbuf = atoi( argv[3] );
int len = sizeof( sendbuf );
setsockopt( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof( sendbuf ) );
getsockopt( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, ( socklen_t* )&len );
printf( "the tcp send buffer size after setting is %d\n", sendbuf );

// 主动发起连接成功
if ( connect( sock, ( struct sockaddr* )&server_address, sizeof( server_address ) ) != -1 )
{
char buffer[ BUFFER_SIZE ];
memset( buffer, 'a', BUFFER_SIZE );
send( sock, buffer, BUFFER_SIZE, 0 ); // 发送数据
}

close( sock ); // 关闭连接
return 0;
}

修改TCP接收缓存区的大小:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 5-11set_recv_buffer.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define BUFFER_SIZE 1024

int main( int argc, char* argv[] )
{
if( argc <= 3 )
{
printf( "usage: %s ip_address port_number receive_buffer_size\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( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int recvbuf = atoi( argv[3] );
int len = sizeof( recvbuf );

// 设置socket属性,设置接收缓冲区大小
setsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof( recvbuf ) );
getsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, ( socklen_t* )&len );
printf( "the receive buffer size after settting is %d\n", recvbuf );

// 绑定socket地址与socket文件描述符
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );

// 监听socket连接
ret = listen( sock, 5 );
assert( ret != -1 );

// 客户端socket地址
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );

// 接收连接accept调用
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{ // 接收连接成功
char buffer[ BUFFER_SIZE ];
memset( buffer, '\0', BUFFER_SIZE );
while( recv( connfd, buffer, BUFFER_SIZE-1, 0 ) > 0 ){}
close( connfd ); // 关闭客户端连接
}

close( sock ); // 关闭本地连接
return 0;
}

服务器端运行5-11set_recv_buffer.cpp(set_recv_buffer程序),在客户端上运行5-10set_send_buffer.cpp(set_send_buffer程序),客户端向服务器发送512字节的数据。

1
2
3
4
5
$./set_recv_buffer 192.168.1.108 12345 50#将TCP接收缓冲区的大小设置为 50字节
the tcp receive buffer size after settting is 256

$./set_send_buffer 192.168.1.108 12345 2000#将TCP发送缓冲区的大小设置 为2000字节
the tcp send buffer size after setting is 4000

从服务器的输出来看,系统允许的TCP接收缓冲区最小为256字节。当我们设置TCP接收缓冲区的大小为50字节时,系统将忽略我们的设置。从客户端的输出来看,我们设置的TCP发送缓冲区的大小被系统增加了一倍。

SO_RCVLOWAT和SO_SNDLOWAT选项

SO_RCVLOWAT选项:TCP接收缓冲区的低水位标记,可读数据总数大于其低水位标记,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据。
SO_SNDLOWAT选项:TCP发送缓冲区的低水位标记,缓冲区的空闲空间(可写入数据的空间)大于其低水位标记,I/O复用系统调用将通知应用程序可以往对应的socke上写入数据。
一般被I/O复用系统调用,用来判断socket是否可读或可写

默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。

SO_LINGER选项

SO_LINGER选项:用于控制close系统调用在关闭TCP连接时的行为。

默认情况下,使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方。

设置(获取)SO_LINGER选项的值时,需要给setsockopt(getsockopt)系统调用传递一个linger类型的结构体,其定义如下:

1
2
3
4
5
#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来判断残留数据是否已经发送完毕。

网络信息API

socket地址的两个要素,即IP地址和端口号,都是用数值表示的。这不便于记忆,也不便于扩展(比如从IPv4转移到IPv6)。可以用主机名来访问一台机器,而避免直接使用其IP地址,用服务名称来代替端口号。比如,下面两条telnet命令具有完全相同的作用:

1
2
telnet 127.0.0.1 80 
telnet localhost www

telnet客户端程序,通过调用某些网络信息API,来实现主机名到IP地址的转换,以及服务名称到端口号的转换。

gethostbyname和gethostbyaddr

gethostbyname函数:根据主机名称获取主机的完整信息。通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器。
gethostbyaddr函数:根据IP地址获取主机的完整信息。

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
#include<netdb.h>

struct hostent* gethostbyname(const char* name);
/*
参数name:指定目标主机的主机名
返回值:指向hostent结构体类型
*/

struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
/*
参数:
addr:指定目标主机的IP地址
len:IP地址的长度
type:IP地址的类型,合法取值有AF_INET(用于IPv4地址)和AF_INET6(用于IPv6地址)
返回值:指向hostent结构体类型
*/

// hostnet结构体定义:
#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函数:根据端口号获取某个服务的完整信息。
它们实际上都是通过读取/etc/services文件来获取服务的信息的。这两个函数的定义如下:

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
#include<netdb.h>

struct servent* getservbyname(const char* name, const char* proto);
/*
参数:
name:指定服务的名字
proto:指定服务类型
返回值:指向servent结构体类型
*/

struct servent* getservbyport(int port, const char* proto);
/*
参数:
port:指定目标服务对应的端口号
proto:指定服务类型
返回值:指向servent结构体类型
*/

// servent结构体定义:
#include<netdb.h>
struct servent {
char* s_name; /*服务名称*/
char** s_aliases; /*服务的别名列表,可能有多个*/
int s_port; /*端口号*/
char* s_proto; /*服务类型,通常是tcp或者udp*/ };

通过主机名和服务名来访问目标服务器上的daytime服务,以获取该机器的系统时间:

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
36
37
38
39
40
41
42
// 5-12access_daytime.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>

int main( int argc, char *argv[] )
{
assert( argc == 2 );
char *host = argv[1];

// 获取目标主机的地址信息
struct hostent* hostinfo = gethostbyname( host );
assert( hostinfo );

// 获取daytime服务信息
struct servent* servinfo = getservbyname( "daytime", "tcp" );
assert( servinfo );
printf( "daytime port is %d\n", ntohs( servinfo->s_port ) );

// 创建socket地址
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
// 因为h_addr_list本身是使用网络字节序的地址列表,所以使用其中的IP地址时,无须对目标IP地址转换字节序
address.sin_addr = *( struct in_addr* )*hostinfo->h_addr_list;

// 创建socket文件描述符,主动发起连接
int sockfd = socket( AF_INET, SOCK_STREAM, 0 );
int result = connect( sockfd, (struct sockaddr* )&address, sizeof( address ) );
assert( result != -1 );

char buffer[128];
result = read( sockfd, buffer, sizeof( buffer ) );
assert( result > 0 );
buffer[ result ] = '\0';
printf( "the day item is: %s", buffer );
close( sockfd );
return 0;
}

注:以上讨论的4个函数都是不可重入的,即非线程安全的。不过netdb.h头文件给出了它们的可重入版本。这些函数的函数名是在原函数名尾部加上_r(re-entrant)。

getaddrinfo

getaddrinfo函数:既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。
是否可重入取决于其内部调用的gethostbyname和getservbyname函数是否是它们的可重入版本。该函数的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<netdb.h>
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反馈的结果(传出参数)。
返回值:
成功时返回0,失败则返回错误码
*/

// addrinfo结构体定义:
struct addrinfo {
int ai_flags; /*标志位,可以按位与*/
int ai_family; /*地址族*/
int ai_socktype; /*服务类型,SOCK_STREAM或SOCK_DGRAM*/
int ai_protocol; /*具体的网络协议,通常设置为0*/
socklen_t ai_addrlen; /*socket地址ai_addr的长度*/
char* ai_canonname; /*主机的别名*/
struct sockaddr* ai_addr; /*指向socket地址*/
struct addrinfo* ai_next; /*指向下一个sockinfo结构的对象*/
};

image-20220725111725849

使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。

利用hints参数获取主机ernest-laptop上的“daytime”流服务信息:

1
2
3
4
5
struct addrinfo hints;
struct addrinfo* res;
bzero(&hints, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("ernest-laptop", "daytime", &hints, &res);

getaddrinfo将隐式地分配堆内存(可以通过valgrind等工具查看),因为res指针原本是没有指向一块合法内存的,所以,getaddrinfo调用结束后,我们必须使用如下配对函数来释放这块内存:

1
2
#include<netdb.h>
void freeaddrinfo(struct addrinfo* res);

getnameinfo

getnameinfo函数:通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<netdb.h> 
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
/*
参数:
sockaddr:socket地址结构体
addrlen:socket地址长度
host:存储主机名
hostlen:主机名长度
serv:存储服务名
servlen:服务名长度
flags:控制getnameinfo的行为
返回值:
成功时返回0,失败则返回错误码
*/

image-20220725112716763

image-20220725112747241

Linux下strerror函数能将数值错误码errno转换成易读的字符串形式。同样,下面的函数可将表5-8中的错误码转换成其字符串形式:

1
2
#include<netdb.h>
const char* gai_strerror(int error);