0%

Linux高性能服务器编程

TCP/IP协议族

TCP/IP协议族体系结构以及主要协议

四层协议模型:

Linux网络编程基础API

socket地址API

主机字节序和网络字节序

  • 大端字节序:高存低,低存高

  • 小端字节序:高存高,低存低

  • 主机字节序—-小端存储

  • 网络字节序—-大端存储

  • 一台机器上的两个进程间通信也需要字节序转换(jvm就是大端存储)

  • 主机字节序和网络字节序转换API

    1
    2
    3
    4
    5
    #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);
    • 长整型通常用来转换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
28
29
30
31
32
#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\n");
}
else if ((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1))
{
printf("little endian\n");
}
else
{
printf("unknown....\n");
}
}

int main(int argc, const char **argv)
{

byteorder();

return 0;
}

通用socket地址

socket网络编程接口中表示socket地址的是结构体sockaddr,定义如下:

1
2
3
4
5
6
7
#include <bits/socket.h>

struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
}
  1. sa_family成员是地址族类型变量。

    协议族 地址族 描述
    PF_UNIX AF_UNIX UNIX本地域协议族
    PF_INET AF_INET TCP/IPv4协议族
    PF_INET6 AF_INET6 TCP/IPv6协议族
  • 宏PF_ 和 AF_都定义在bis/socket.h文件中,两者值完全相同,所以常常混用
  • sa_data成员用于存放socket地址值。但是不同的协议族地址值有不同含义和长度
    协议族 地址值含义和长度
    PF_UNIX 文件的路径名,长度可达108字节
    PF_INET 16bit端口号和32bitIPv4地址,共6字节
    PF_INET6 16bit端口号、32bit流标识、128bitIPv6地址,32bit范围ID,共26字节

下边也是通用的socket结构体

1
2
3
4
5
6
7
#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地址不好用,用Linux提供的专用socket地址结构体

  1. sockaddr_un 为 本地套接字
  2. sockaddr_in 为 ipv4
  3. sockaddr_in6 为 ipv6

重点为ipv4:

1
2
3
4
5
6
7
8
9
10
struct sockaddr_in
{
sa_family_t sa_family; //地址族:AF_INET
u_int16_t sin_port; //端口号,要用网络字节序表示
struct in_addr sin_addr;//IPv4地址结构体
}
struct in_addr
{
u_int32_t s_addr;//IPv4地址,要用网络字节序表示
}

所有专用socket地址类型变量在使用的时候都必须强转为sockaddr类型

IP地址转换函数

编程中需要把点分十进制(192.168.1.117)转换成整数(二进制数)

ipv4使用以下函数转换:

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
#include <arpa/inet.h>
/**
* 将点分十进制字符串表示的IPv4地址转化为网络字节序整数表示的地址。
* 失败返回INADDR_NONE
**/
in_addr_t inet_addr(const char *strptr);

/**
* 功能和inet_addr一样: 将点分十进制字符串表示的IPv4地址转化为网络字节序整数表示的地址。
* 但是将转化结果存储与参数inp指向的地址结构中
* 成功返回 1
* 失败返回 0
**/
int inet_aton(const* cp,struct in_addr* inp);

/**
* 将网络字节序整数表示的IPv4地址转化为点分十进制字符串表示的地址。
* 但是将转化结果存储与参数inp指向的地址结构中
* 该函数不可重入
**/
char *inet_ntoa(struct in_addr in);

/**
* 将点分十进制字符串src表示的IP(v4、v6)地址转化为网络字节序整数表示的地址,并把转换结果存储与dst指向的内存中。
* af:地址族
* 成功返回 1
* 失败返回 0 errno
**/
int inet_pton(int af,const char*src,void *dst);

/**
* 与inet_pton作相反转换
**/
const char *inet_ntop(int af,const void *src,char *dst,socklen_t cnt);

创建socket(socket函数)

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <sys/socket.h>
/*
* domain: 协议族 PF_INET、PF_INET6、PF_UNIX
* type: 服务类型 SOCK_STREAM、SOCK_DGRAM
* protocol: 0
* 返回值: 成功返回 文件描述符, 失败返回-1, errno
*/
int socket(int domain, int type, int protocol);

命名socket(bind函数)

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <sys/socket.h>
/*
* bind绑定失败时, 会返回errno
* 常见的errno:
* EACCES 被绑定的地址是受保护的
* EADDRINUSE 被绑定的地址正在使用中, 比如将地址绑定到处于TIME_WAIT状态的socket地址
*/
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

监听socket(listen函数)

1
2
3
4
5
/*
* backlog 表示内核监听队列的最大长度(只表示完全连接状态的socket上限)
*/
#include <sys/socket.h>
int listen(int sockfd, int backlog);

一下代码是backlog参数对listen的影响

处于ESTABLISHED(数据传输)状态的为backlog+1

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
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>

static bool stop = false;
static void handler_term(int sig)
{
stop = true;
}

int main(int argc, char *argv[])
{
signal(SIGTERM, handler_term);

if (argc <= 3)
{
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
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);

struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = PF_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);

while (!stop)
{
sleep(1);
}

close(sock);

return 0;
}

接受连接(accpet函数)

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>
#include <sys/socket.h>
/*
* sockfd listen函数中的socket
* addr 远端socket地址
* addrlen 远端socket地址长度
* 返回值
* 成功 返回一个新的连接socket
* 失败 返回 -1 并设置 errno
* 成功返回的socket唯一标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信
*/
int accept(int sockfd, struct sockaddr* addr,socklen_t* addrlen);

如果监听队列中处于ESTABLISTEN状态的客户端断开连接,accept()函数依然可以成功返回。accept()函数只是从监听队列中取出连接,不管连接处于何种状态

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
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, const char **argv)
{

int server_sock;
struct sockaddr_in server_addr;

server_sock = socket(PF_INET, SOCK_STREAM, 0);
assert(server_sock >= 0);

server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;

int ret = bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
assert(ret != -1);

ret = listen(server_sock, 5);
assert(ret != -1);

sleep(20);
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(client_addr);
int connfd = accept(server_sock, (struct sockaddr *)&client_addr, &client_addrlen);
if (connfd < 0)
{
printf("errno is %d\n", errno);
}
else
{
char remote[INET_ADDRSTRLEN];
printf("connect with ip : %s and port is : %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, remote, INET_ADDRSTRLEN),
ntohs(client_addr.sin_port));

close(connfd);
}
close(server_sock);

return 0;
}

发起连接

客户端主动与服务端建立连接

1
2
3
4
5
6
7
8
9
10
11
#include <sys/types.h>
#include <sys/socket.h>
/*
* sockfd 客户端使用socket新建的sockfd
* serv_addr 服务器监听的地址
* addr_len 地址长度
* 返回值
* 成功 返回 0
* 失败 返回 -1 并设置 errno
*/
int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t addrlen);
  • 常见errno
    • ECONNREFUSED: 目标端口不存在,连接被拒绝
    • ETIMEDOUT: 连接超时

关闭连接

不是立即关闭一个连接,而是将fd的引用数-1,fd的引用为0时才是真正关闭

1
2
#include<unistd.h>
int close(int fd);

立即关闭连接

1
2
#include <sys/socket.h>
int shutdown(int sockfd, int howto);

数据读写

TCP数据读写

对比read、write函数增加了对数据读写的控制。

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd,void *buf,size_t len,int flag);
ssize_t send(int sockfd,const void *buf)

flag参数为数据读写提供了额外的控制

选项名 含义 send recv
MSG_CONFIRM 指示数据链路层协议持续监听对方的回应,直到得到答复,它仅能用于SOCK_DGRAM和SOCK_RAW类型的socket Y N
MSG_DONTROUTE 不查看路由表,直接将数据发送给本地局域网内的主机。这表示发送者确切的知道目标主机就在本地网络上 Y N
MSG_DONTWAIT 对socket的此次操作将是非阻塞的 Y Y
MSG_MORE 告诉内核应用程序还有更多数据要发送,内核将超时等待新数据写入TCP发送缓冲区后一并发送。这样可以防止TCP发送过多小的报文段,从而提高传输效率 Y N
MSG_WAITALL 读操作仅在读取到指定数量的字节后才返回 N Y
MSG_PEEK 窥探读缓存中的数据,此次读操作不会导致这些数据被清除 N Y
MSG_OOB 发送或接收紧急数据 Y Y
MSG_NOSIGNAL 往读端关闭的管道或者socket连接中写数据时不引发SIGPIPE信号 Y N
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
//发送带外数据
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <unistd.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;
}
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;
server_address.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_address.sin_addr);

int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
if (connect(sockfd, (struct sockaddr *)&server_address,
sizeof(server_address)) < 0)
{
printf("connect failed\n");
}
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;
}
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
//接收带外数据
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.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;
}
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 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);

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

close(sock);
return 0;
}

UDP数据读写

1
2
3
4
5
6
#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和sendto也可用于面向连接的socket,只需要把最后两个参数设置为NULL以忽略发送端/接收端的socket地址

通用数据读写

以下两个函数不仅能用于TCP流数据,也能用于UDP数据报

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int 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参数,并在调用过程中更新
}

msg_name参数对于TCP来说不用设置。

带外标记

实际应用中无法预期带外数据何时到来。

1
2
3
4
//判断sockfd是否带外标记
//是 返回 1
#include <sys/socket.h>
int sockatmark(int sockfd);

地址信息函数

想知道连接socket的本端socket地址,以及远端socket地址,用以下函数

1
2
3
4
5
#include <sys/socket.h>
//获取本端socket地址
int getsockname(int sockfd, struct sockaddr* address,socklen_t* address_len);
//获取远端socket地址
int getpeername(int sockfd, struct sockaddr* address,socklen_t* address_len);

socket选项

以下函数为socket文件描述符属性读取和设置专用

1
2
3
4
5
6
7
8
9
#include <sys/socket.h>

//level参数指定要操作哪个协议的选项(IPv4、IPv6、TCP等)
//option_name参数指定选项名字
//option_value指定被操作选项的值、option_len长度
//成功返回0、失败返回-1,并设置errno
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* restrict option_len);

image-20210721193914528

部分socket选项设置应该在listen/connect函数之前完成

SO_REUSEADDR选项

主动关闭连接端会经历TIME_WAIT状态,这个时候可以用SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。

1
2
3
4
5
6
7
8
9
10
11
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSERADDR,&reuse,sizeof(reuse));

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_in*)&address,sizeof(address));
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
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

int main(int argc, char **argv)
{
if (argc < 2)
{
printf("Usage: %s ip_address, port_number\n", basename(argv[0]));
return 1;
}

char *ip = argv[1];
int port = atoi(argv[2]);

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

struct sockaddr_in server_address;
bzero(&server_address, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_address.sin_addr);

int ret;
ret = bind(sockfd, (struct sockaddr *)&server_address, sizeof(server_address));
assert(ret != -1);

ret = listen(sockfd, 5);
assert(ret != -1);

struct sockaddr_in client_address;
socklen_t client_address_len = sizeof(client_address);
int connfd = accept(sockfd, (struct sockaddr *)&client_address, &client_address_len);
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_address.sin_addr,
remote, INET_ADDRSTRLEN),
htons(client_address.sin_port));
close(connfd);
}
close(sockfd);
return 0;
}

SO_RCVBUF和SO_SNDBUF选项

设置TCP接收、发送缓冲区大小。使用setsockopt设置缓冲区大小时,系统都会将其值翻倍,并且不得小于某个值。

  • TCP接收缓冲区最小值为256字节
  • TCP发送缓冲区最小值为2048字节

缓冲区最小值取决于操作系统。

为了保证一个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
//服务端,设置接收缓冲区
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>

#define BUFFER_SIZE 1024

int main(int argc, char **argv)
{
if (argc <= 2)
{
printf("Usage: %s ip port buffer_size\n", basename(argv[0]));
return 1;
}

const char *ip = argv[1];
int port = atoi(argv[2]);

struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &addr.sin_addr);

int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

int recvbuf = atoi(argv[3]);
int len = sizeof(recvbuf);

setsockopt(sock, SOL_SOCKET, SO_RCVBUF,
&recvbuf, sizeof(recvbuf));
getsockopt(sock, SOL_SOCKET, SO_RCVBUF,
&recvbuf, (socklen_t *)&len);
printf("the tcp receive buffer size after setting is %d\n", recvbuf);

int ret = bind(sock, (struct sockaddr *)&addr,
sizeof(addr));
assert(ret != -1);

ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);

int connfd = accept(sock, (struct sockaddr *)&client_addr,
&client_addr_len);
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;
}
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
//客户端,设置发送缓冲区
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

#define BUFFER_SIZE 512

int main(int argc, char **argv)
{
if (argc <= 2)
{
printf("Usage:%s ip_address port_number buffer_size\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;
server_address.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_address.sin_addr);

int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

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;
}

SO_RCVLOWAT和SO_SNDLOWAT选项

表示TCP接收缓冲区和发送缓冲区的低水位标记,一般被I/O复用系统调用用来判断socket是否可读可写。当TCP接收缓冲区中可读数据的总数大于低水位标记,I/O复用系统调用将通知应用程序可以冲对应的socket上读取数据;当TCP发送缓冲区中的空间大于低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写入数据。

默认情况,低水位标记为1字节。

SO_LINGER选项

用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当我们使用close系统调用来关闭一个socket是,close将立即返回,TCP模块扶着将该socket对用的TCP发送缓冲区残留的数据发送给对方。

设置SO_LINGER选项的值时,需要给setsockopt传递一个linger类型的结构体。

1
2
3
4
5
6
#include <sys/socket.h>
struct linger
{
int l_onoff; //开启(非0)还是关闭(0)该选项
int l_linger; //滞留时间
}

根据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是否有残留数据
    • 该socket是阻塞还是非阻塞的
      • 对于阻塞:close将等待l_linger时间,直到TCP模块发送完所有残留数据并得到对方确认
      • 对于非阻塞:close立即返回

网络信息API

socket的IP地址和端口号不便于记忆与扩展,所以使用主机名和服务名称来代替IP地址和端口号

gethostbyname、gethostbyaddr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <netdb.h>
//根据主机名获取主机完整信息
//name : 目标主机名
struct hostent *gethostbyname(const char *name);

//根据IP地址获取主机完整信息
//addr : 目标主机IP地址
//len : IP地址长度
//type : IP地址的类型(AF_INET、AF_INET6)
struct hostent *gethostbyaddr(const void *addr, size_t len, int type);

//返回值为hostent结构体
#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根据端口号获取服务的完整信息。都是通过读取/exc/services文件夹来获取服务信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <netdb.h>
//name:目标服务名字
//port:端口号
//proto:服务类型 tcp、udp
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);

//返回struct servent*
#include <netdb.h>
struct servent
{
char *s_name; //服务名称
char **s_aliases; //服务的别名列表,可能有多个
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
#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 );
struct servent* servinfo = getservbyname( "daytime", "tcp" );
assert( servinfo );
printf( "daytime port is %d\n", ntohs( servinfo->s_port ) );

struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *( struct in_addr* )*hostinfo->h_addr_list;

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 tiem is: %s", buffer );
close( sockfd );
return 0;
}

上述四个函数属于不可重入函数(非线程安全),可重入版本在函数名称后边加上_r即可

1
2
3
4
struct hostent *gethostbyname_r(const char *name);
struct hostent *gethostbyaddr_r(const void *addr, size_t len, int type);
struct servent *getservbyname_r(const char *name, const char *proto);
struct servent *getservbyport_r(int port, const char *proto);

getaddrinfo

该函数既能通过主机名获取IP地址,也能通过服务名获得端口号。它是否可重入取决于内部调用gethostbyname、getservbyname函数是否是可重入版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <netdb.h>
//hostname:可以接收主机名,也可以接收字符串表示的IP地址
//service:可以接收服务名,也可以接收字符串表示的十进制端口号
//hints:是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精准的控制,可以设置为NULL
//result:指向一个链表,用于存储getaddrinfo反馈的结果
int getaddrinfo(const char *hostname, const char *service,
const struct addrinfo *hints, struct addrinfo **result);

struct addrinfo
{
int ai_flags; /* Input flags. */
int ai_family; /* 地址族 */
int ai_socktype; /* 服务类型, SOCK_STREAM或SOCK_DFRAM */
int ai_protocol; /* 具体的网络协议,含义和socket系统调用的第三参数相同,设置为0 */
socklen_t ai_addrlen; /* socket地址ai_addr的长度 */
struct sockaddr *ai_addr; /* 指向socket地址 */
char *ai_canonname; /* 主机的别名 */
struct addrinfo *ai_next; /* 指向下一个sockinfo结构的对象 */
};

ai_flags值可以按下表的标志的按位或

image-20210724162958378

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

1
2
3
4
5
6
7
8
9
10
struct addrinfo hints;
struct addrinfo *res;

bzero(&hints,sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("ernest-laptop","daytime",&hints,&res);

//因为*res不是指向一块合法内存的,所以调用结束后要释放内存
#include <netdb.h>
void freeaddrinfo(struct addrinfo* res);

getnameinfo

该函数能通过socket地址通知获得以字符串表示的主机名(内部使用的gethostbyaddr函数)和服务名(内部使用getservbyport函数)。是否可重入同上

1
2
3
#include <netdb.h>
int getnameinfo(const struct sockaddr *sockaddr,socklen_t addrlen, char *host,
socklen_t hostlen, char *serv,socklen_t servlen, int flags);

genameinfo将返回的主机名存储在host参数指向的缓存中,将服务名存储在serv参数指向的缓存中,hostlen和servlen参数分别指定这两块缓存的长度。

flag参数控制getnameinfo的行为。flag取值如下:

image-20210724164506566

getaddrinfo和getnameinfo函数成功返回0,失败返回错误码:

image-20210724164615865

Linux下strerror函数能将数值错误码errno转换成易读的字符串形式。下面函数可将上表的错误码转换成其字符串形式

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

高级I/O函数

  • 用于创建文件描述符的函数,pipe、dup/dup2

  • 用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee

  • 用于控制I/O行为和属性的函数,包括fcntl函数

pipe函数

pipe函数可用于创建一个管道,以实现进程间通信。

1
2
3
#include <unistd.h>
//成功返回0,失败返回-1 并设置errno
int pipe(int fd[2]);

fd[0]和fd[1]分别构成管道两端,往fd[1]写入的数据可以从fd[0]读出,并且fd[0]只能从管道读出数据,fd[1]只能用于往管道写入数据。如果要实现双向的数据传输,应该使用两个管道。默认情况下,这一对文件描述符是阻塞的,如果我们使用read来读取一个空的管道,则将被read阻塞,直到管道有数据可读;使用write写一个满的管道也是一样。如果程序将这对fd设置为非阻塞,则read和write会有不同行为

管道内部传输的是字节流,管道容量的默认大小:65536字节

socketpair函数能够方便的创建双向管道。创建的一对文件描述符都是可读可写的。

1
2
3
4
#include <sys/type.h>
#include <sys/socket.h>
//domain : AF_UNIX 只能在本地使用
int socketpair(int domain, int type, int protocol, int fd[2]);

dup函数和dup2函数

这两可以把标准输入重定向到一个文件或者把标准输入重定向到一个网络连接(CGI编程)

1
2
3
4
#include<unistd.h>
//失败返回-1 并设置errno
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
  • dup:创建一个新的文件描述符,改文件描述符和原油文件描述符file_descriptor指向相同文件、管道或者网络连接,并且返回的文件描述符总是去取系统当前可用的最小整数值。
  • dup2:与dup类似,返回第一个不小于file_descriptor_two的整数值。
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
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
if (argc <= 2)
{
printf("Usage: %s ip port\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;
server_address.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_address.sin_addr);

int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

int ret = bind(sock, (struct sockaddr *)&server_address,
sizeof(server_address));
assert(ret != -1);

ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client_address;
socklen_t client_address_len;
int connfd = accept(sock, (struct sockaddr *)&client_address, &client_address_len);
if (connfd < 0)
{
printf("errno is: %d\n", errno);
}
else
{
close(STDOUT_FILENO);
dup(connfd);
printf("abcdefg\n");
close(connfd);
}

close(sock);
return 0;
}

readv函数和writev函数

readv函数

sendfile

mmap函数munmap函数

splice函数

tee函数

fcntl函数

Linux服务器程序规范

  • Linux服务器程序一般以后台进程(守护进程)形式运行
  • 通常有一套日志系统
  • 以某个专门的非root身份运行
  • 通常可配置
  • 启动的时候生成一个PID文件并存入/var/run目录中
  • 需要考虑系统资源和限制
-------------THE END-------------

欢迎关注我的其它发布渠道