Basic Network I/O Techniques

经常能在各种地方看到这么一句话:

Everything in Unix is a file

当Unix程序做任何的I/O时,本质上他们也是通过读/写相应的文件的描述符(file descriptor)来进行操作。

在Unix系统中,文件描述符是一个泛化的概念,它是一个代表了某个打开的‘文件’的一个整型值(integer)。此处的‘文件’既可以是一个字面意义上存储在磁盘上的一个数据文件,也可以是一个网络通信,一个FIFO,一个Pipe…等。

本文中,我将研究一下基本的C语言下的Network I/O。

Socket是系统中一种使用文件描述符来与其它程序进行通信的方式。 通常来说,我们会使用Socket来进行网络中传输层的数据传输,即使用TCP或者UDP协议,来进行网络的通信。

要通过网络进行数据传输,不管是否需要进行连接,我们都至少需要知晓对应的数据接收方的地址。网络环境下,我们需要使用对应的IP来路由到对应接受程序所在的主机(Host)地址,而后再使用端口(Port)来定位到对应的程序。当下的环境中,我们存在IPv4与IPv6等不同的IP协议版本,或者我们可能使用不同的协议(protocol)进行通信,因此,我们需要通过一些通信元数据的配置,来获得我们需要的Socket,从而使用Socket来进行网络通信。

总体说来,使用Socket进行数据通信有着如下的基本流程:

  • 获得对应的用来告诉Socket地址信息的元数据 struct addrinfo
  • 使用上一步获得的地址信息,创建一个可用的Socket
  • 如果是一个服务端应用
    • 当前的Socket,通常是使用本机的地址信息进行创建的
    • 使用bind将该程序绑定到系统可用的某个端口,使其它网络程序可以访问
    • 使用listen来声明我们将在当前主机的对应端口来进行监听
    • 我们可以调用一个阻塞的accept操作,一直到有其他方connect到我们监听的端口,此时会产生一个用来描述这个新连接的socket file descriptor
    • 通过这个新连接得到的socketfd,使用send/recv进行通信(如果使用无连接UDP,则使用对应的sendto/recvfrom操作)
  • 如果是一个客户端应用
    • 当前的Socket,通常是使用目标服务地址信息进行创建的
    • 使用connect进行到服务端的连接
    • 通过这个新连接得到的socketfd,使用send/recv进行通信(如果使用无连接UDP,则使用对应的sendto/recvfrom操作)
  • 使用close或者shutdown关闭创立的Socket连接

struct addrinfo

网络通信,需要知晓通信方对应的地址,再通过创建对应的Socket来建立连接,进行通信。struct addrinfo就是对应的用来告诉Socket地址信息的元数据。

1
2
3
4
5
6
7
8
9
10
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // use 0 for "any"
size_t ai_addrlen; // size of ai_addr in bytes
struct sockaddr *ai_addr; // struct sockaddr_in or _in6
char *ai_canonname; // full canonical hostname
struct addrinfo *ai_next; // linked list, next node
};

这个结构体里,我们可以指定对应的IP协议(IPv4 or IPv6),socket的类型(TPC or UDP),以及其它地址相关的信息。我们需要通过填入对应的元信息,再使用这个addrinfo来进行Socket的创建。当然,我们不需要手动的进行填写,我们可以使用getaddrinfo函数来进行数据的填充:

1
2
3
4
int getaddrinfo(const char *node,     // e.g. "www.example.com" or IP
const char *service, // e.g. "http" or port number
const struct addrinfo *hints, // pointer to a addrinfo that you have already fill out with relevant information
struct addrinfo **res); // pointer to a linked-list of getaddrinfo results

例如,我们可以用如下方式来获得一个绑定本机IP以及特定PORT的addrinfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
int status;
char *MYPORT="6666";
struct addrinfo hints, *servinfo;

memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_flags = AI_PASSIVE; // fill in local IP
if ((status = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 1;
}

或者,获得对应一个网络端的addrinfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
int status;
char *IP="www.example.com";
char *PORT="6666";
struct addrinfo hints, *servinfo;

memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
if ((status = getaddrinfo(IP, PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 1;
}

通过以上的操作,我们将获得res,即上例的servinfo, 这是一个addrinfo的链表。我们需要遍历这个链表来找到一个可用的信息来创建Socket。需要注意的是,对于锁获得的res,即上述的servinfo,我们需要调用 freeaddrinfo(servinfo) 来清理对应的数据。

在获得对应的信息后,我们就可以依据上述流程,创建对应的Socket,我们将得到一个socketfd。之后,我们就可以对socketfd这个文件描述符进行相应的网络I/O操作。需要注意的是,对于每一步操作,都需要检验其返回值,来检查操作是否成功的执行或者资源是否可用。

Demo TCP Server

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/* 
* Stream socket server demo
* Usage: telnet localhost 3490
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define MYPORT "3490" // the port clinets will be connecting to
#define BACKLOG 5 // how many pending connections queue will hold
#define DEFAULT_FLAG 0 // default sent/recv flag

// get socket addr, IPv4 or IPv6
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET)
{
return &(((struct sockaddr_in *)sa)->sin_addr);
}

return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}

int main(int argc, char **argv)
{
int sockfd, new_fd; // listen on sockfd, new connection on new_fd
struct addrinfo hints, *servinfo, *p; // server info & iter pointer
struct sockaddr_storage client_addr; // connector's address information
socklen_t addr_size; // length of connecting socket
int status; // status for checking
int yes = 1;
char s[INET6_ADDRSTRLEN];

// 1. load up local address info
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_flags = AI_PASSIVE; // fill in local IP
if ((status = getaddrinfo(NULL, MYPORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 1;
}

// 2. loop through results list and bind to the first we can
for (p = servinfo; p != NULL; p = p->ai_next)
{
// 2.1. create an available socket
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
perror("server: socket");
continue;
}

// 2.2 set sock opt
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
{
perror("setsockopt");
exit(1);
}

// 2.3. bind socket to port
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("server: bind");
continue;
}

break;
}
freeaddrinfo(servinfo); // clean up memory
if (p == NULL)
{
fprintf(stderr, "server: failed to bind\n");
exit(1);
}

// 3. listen
if (listen(sockfd, BACKLOG) == -1)
{
perror("listen");
exit(1);
}

fprintf(stdout, "Waiting for connection...\n");

// 4. accept incoming messages
while (1)
{
// 4.1. accept new connection
addr_size = sizeof client_addr;
if ((new_fd = accept(sockfd, (struct sockaddr *)&client_addr, &addr_size)) == -1)
{
perror("accept");
continue;
}

// 4.2. get client address
inet_ntop(client_addr.ss_family, get_in_addr((struct sockaddr *)&client_addr), s, sizeof s);
fprintf(stdout, "server: got connection from %s\n", s);

// 4.3. fork new process to handle, exit after processing
if (!fork()) // child process
{
close(sockfd); // child doesn't need the listener
// communicate via send/recv (for unconnected UDP, use sendto, recvfrom)
if (send(new_fd, "Hello World!", 13, DEFAULT_FLAG) == -1)
{
perror("send");
}
close(new_fd);
exit(0);
}
close(new_fd); // parent doesn't need new_fd; if want to customize, use int shutdown(int sockfd, int how)
}

return 0;
}

Demo TCP Client

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/* 
* Stream socket client demo
* Usage: ./client localhost
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define PORT "3490" // the port clinets will be connecting to
#define MAXDATASIZE 100 // max number of bytes we can get at once
#define DEFAULT_FLAG 0 // default sent/recv flag

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET)
{
return &(((struct sockaddr_in *)sa)->sin_addr);
}

return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}

int main(int argc, char **argv)
{
int sockfd, numbytes; // socket file descriptor
char buf[MAXDATASIZE]; // buf for data recv
struct addrinfo hints, *servinfo, *p; // server info & iter pointer
int status;
char s[INET6_ADDRSTRLEN];

if (argc != 2)
{
fprintf(stderr, "usage: client hostname\n");
exit(1);
}

// 1. load up server address info
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
if ((status = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 1;
}

// 2. loop through results list and bind to the first we can
for (p = servinfo; p != NULL; p = p->ai_next)
{
// 2.1. create an available socket
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
perror("client: socket");
continue;
}

// 2.2. connect
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("client: connect");
continue;
}

break;
}
if (p == NULL)
{
fprintf(stderr, "client: failed to connect\n");
exit(1);
}
inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr), s, sizeof s);
fprintf(stdout, "client: connecting to %s\n", s);
freeaddrinfo(servinfo); // clean up memory

// 3. recv message from server
if ((numbytes = recv(sockfd, buf, MAXDATASIZE - 1, DEFAULT_FLAG)) == -1)
{
perror("recv");
exit(1);
}

buf[numbytes] = '\0';
fprintf(stdout, "client: received '%s'\n", buf);
close(sockfd);

return 0;
}

Ref

Ref. Beej’s Guide to Network Programming