网络编程 - CS:APP 第十一章

网络编程 - CS:APP 第十一章

客户端和服务器都是进程,而不是机器或主机。

IP地址

IP地址结构:由于早期的一些原因,存放IP地址的32位无符号整数被放到了一个结构中

1
2
3
struct in_addr {
uint32_t s_addr; // 32位IP地址,使用网络字节顺序(大端法)
}

由于不同的设备,使用的字节顺序不同。因此,在网络上进行传输时,会首先把字节转换成网络字节顺序,接受时,会再转换回本地字节顺序。

字节顺序和主机顺序的转换:

1
2
3
4
5
6
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // host to net
uint16_t htons(uint16_t hostshort); // host to net

uint32_t ntohl(uint32_t netlong); // net to host
uint16_t ntohs(uint16_t netshort); // net to host

点分IP地址与32位IP地址的转换:

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

// 点分十进制转换成32位地址
/*
得到的32位地址也要转换成本地顺序
返回:若成功返回1,串非法返回0,错误返回-1
*/
int inet_pton(AF_INET, const char *src, void *dst);

// 32位地址转换成点分十进制
/*
在使用该函数之前,要将32位IP地址转换成网络顺序
把指向src的32位IP地址转换成点分十进制地址,并把得到的以NULL结尾的字符串的做多size个字符赋值到dst中
返回:若成功返回指向点分十进制地址的指针,出错返回NULL
*/
int char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);

  1. 1
    2
    // 将十六进制字符串转换成十进制数:
    使用sscanf(str, "%x", &num)
  2. 使用32位IP地址时,可以直接使用in_addr结构而不是in_addr结构里的s_addr

套接字

是一组绝大多数操作系统都定义的函数,可以实现网络应用。

image-20220923115128790

套接字地址结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这两个结构体所占字节数相同,可以互相转换
struct sockaddr_in {
uint16_t sin_family; // 协议簇类型,通常是 AF_INET(ipv4, ipv6是AF_INET6)
uint16_t sin_port; // 端口号,使用网络字节顺序
struct in_addr sin_addr; // IP地址结构
unsigned char sin_zero[8]; // 填充0,为了满足struct sockaddr的大小
}; // 这个结构体一共占16个字节

struct sockaddr {
uint16_t sa_family;
char sa_data[14];
}

typedef struct sockaddr SA;

socket() 通用

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

int socket(int domain, int type, int protocol);
/*
使套接字称为一个连接的结点
返回一个还不能用于读写的描述符
*/

// 使称为一个连接点
clientfd = socket(AF_INET, SOCK_STREAM, 0);

connect() 客户端

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

int connect(int clientfd, const struct sockaddr * addr, socklen_t addrlen);
/*
客户端clientfd试图与addr建立连接, addrlen是sizeof(sockaddr_in)
成功返回0, 出错返回-1
*/

bind() 服务器

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

int bind(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
/*
将addr中的服务器地址与套接字描述符sockfd连接起来, addrlen是sizeof(sockaddr_in)
成功返回0, 出错返回-1
*/

listen() 服务器

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

int listen(int sockfd, int backlog);
/*
该函数使sockfd变成一个监听套接字,backlog是最大连接数,一般为1024
成功返回0, 出错返回-1
*/

accept() 服务器

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

int accept(int listenfd, struct sockaddr *addr, int *addrlen);
/*
成功返回非负连接描述符, 出错返回-1
*/

建立连接

服务器:

  1. 调用socket(),创建连接结点
  2. 调用bind() 成为一个服务器套接字
  3. 调用listen() 转换成监听描述符
  4. 调用accept() ,阻塞程序,等待客户端连接,返回连接描述符

客户端:

  1. 调用socket(),创建连接结点
  2. 调用connect(), 连接服务器

套接字地址和主机名服务名的转换

首先了解,域名和IP地址是等价的,服务名和端口号是等价的

例如localhost和127.0.0.1是等价的,http和80是等价的

从 字符串主机名服务名信息 到 二进制套接字地址信息

为什么要使用这个函数?

域名和IP地址是多对多的,一个域名可能对应多个IP地址,因此此函数返回的链表很有帮助。

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

/*
host: 域名或点分十进制IP地址
service: 服务名或端口号
hints: 可选 传递设置
result: 指向一个addrinfo 结构链表,每个结构指向一个对应host和service的套接字地址结构
*/
int getaddrinfo(const char *host, const char *service,
const struct addrinfo *hints, struct addrinfo **result);

// 释放返回的链表
void freeaddrinfo(struct addrinfo, *result);

// 将错误代码转换成错误信息
const char *gai_strerror(int errcode);

getaddrinfo 使用的结构

1
2
3
4
5
6
7
8
9
10
11
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;

char *ai_canonname;
size_t ai_addrlen;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
}

从 二进制套接字地址信息 到 字符串主机名服务名信息

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

int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *service, size_t servlen,
int 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
int open_clientfd(char *hostname, char *port) {
int clientfd, rc;
struct addrinfo hints, *listp, *p;

/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a connection */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
return -2;
}

/* Walk the list for one that we can successfully connect to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */

/* Connect to the server */
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* Success */
if (close(clientfd) < 0) { /* Connect failed, try another */ //line:netp:openclientfd:closefd
fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
return -1;
}
}

/* Clean up */
freeaddrinfo(listp);
if (!p) /* All connects failed */
return -1;
else /* The last connect succeeded */
return clientfd;
}
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
int open_listenfd(char *port) 
{
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval=1;

/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
return -2;
}

/* Walk the list for one that we can bind to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */

/* Eliminates "Address already in use" error from bind */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, //line:netp:csapp:setsockopt
(const void *)&optval , sizeof(int));

/* Bind the descriptor to the address */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break; /* Success */
if (close(listenfd) < 0) { /* Bind failed, try the next */
fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
return -1;
}
}


/* Clean up */
freeaddrinfo(listp);
if (!p) /* No address worked */
return -1;

/* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, LISTENQ) < 0) {
close(listenfd);
return -1;
}
return listenfd;
}

例子

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
// echoclient.c
#include <csapp.h>

int main(int argc, char *argv[])
{
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio;

if (argc != 3) {
fprintf(stderr, "argument error\n");
exit(0);
}

host = argv[1];
port = argv[2];

clientfd = Open_clientfd(host, port);
Rio_readinitb(&rio, clientfd);

while (Fgets(buf, MAXLINE, stdin) != NULL) {
Rio_writen(clientfd, buf, strlen(buf));
Rio_readlineb(&rio, buf, MAXLINE);
Fputs(buf, stdout);
}
Close(clientfd);
exit(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
// echoserver.c
#include <csapp.h>

void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;

Rio_readinitb(&rio, connfd);
while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
printf("server received %d bytes\n", (int)n);
printf("Received:%s\n", buf);
Rio_writen(connfd, buf, n);
}
}

int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr;
char client_hostname[MAXLINE], client_port[MAXLINE];

if (argc != 2) {
fprintf(stderr, "argument error\n");
exit(0);
}

listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA*)&clientaddr, &clientlen);
Getnameinfo((SA*)&clientaddr, clientlen, client_hostname, MAXLINE,
client_port, MAXLINE, 0);
printf("connected to (%s, %s)\n", client_hostname, client_port);
echo(connfd);
Close(connfd);
}

exit(0);
}

术语索引

protocol family 协议簇:一组相关联的协议