UNIX网络编程笔记

Posted by Hazuki on 2019-06-23

TCP连接的分组交换

TIME_WAIT状态

主动执行关闭的一端主动执行这个状态,该端点停留在这个状态的持续时间为最长分节生命期(maximum segment lifetime, MSL)的两倍,有时被称为2SML。

存在的理由:

  • 可靠地实现TCP全双工连接的终止
    • 假设上图中最终的ACK丢失,则服务器端会重发FIN来请求客户的ACK。如果客户不维护这些信息,它将响应以一个RST,该分解被服务器解释成一个错误。如果TCP要彻底终止某个连接上两个方向的数据流(全双工关闭),那么它需要正确处理四次挥手中任何一个分节丢失的状况。主动执行关闭的一端可能不得不重发ACK,所以需要进入TIME_WAIT状态。
  • 允许老的重复分节在网络中消逝
    • 假设在某一IP地址的某一端口上有一个连接,该连接关闭之后过了一段时间又再次在该IP和端口上建立了另一个连接,这个连接称为前一个连接的化身。TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一连接的某个新的化身。因此,TCP将不给处于TIME_WAIT状态的连接发起新的化身。而且TIME_WAIT的时间为2SML,足以让这个连接的分组都被丢弃。这样,可以保证在建立一个新的连接时,之前连接的老的重复的分组都已经在网络中消逝了。

IPv4套接字地址结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// <netinet/in.h>
struct in_addr
{
in_addr_t s_addr; // 32-bit IPv4 address network byte ordered
};

struct sockaddr_in
{
unit8_t sin_len; // length of structure(16)
sa_family sin_family; // AF_INET
in_port_t sin_port; // 16-bit TCP or UDP port number network byte ordered
struct in_addr sin_addr; // 32-bit IPv4 address network byte ordered
char sin_zero[8]; // unused
};

POSIX 定义的数据类型

数据类型 说明 头文件
int8_t 带符号的8位整数 <sys/types.h>
uint8_t 无符号的8位整数 <sys/types.h>
int16_t 带符号的16位整数 <sys/types.h>
uint16_t 无符号的16位整数 <sys/types.h>
int 32_t 带符号的32位整数 <sys/types.h>
uint32_t 无符号的32位整数 <sys/types.h>
sa_family_t 套接字地址结构的地址族 <sys/socket.h>
socklen_t 套接字地址结构的长度,一般为uint32_t <sys/socket.h>
in_addr_t IPv4地址,一般为uint32_t <netinet/in.h>
in_port_t TCP或UDP端口,一般为uint16_t <netinet/in.h>

通用套接字地址结构

1
2
3
4
5
6
7
// <sys/socket.h>
struct sockaddr
{
uint8_t sa_len;
sa_family_t sa_family; // address family: AF_XXX value
char sa_data[14]; // protocol-specific address
};

调用bind等函数时,需要将特定协议的套接字地址结构的指针进行强制类型转换,例如:

1
2
3
4
5
struct sockaddr_in serv; //IPv4 socket address structure

/* fill in serv() */

bind(socketfd, (struct sockaddr*) &sockaddr_in, sizeof(serv))

IPv6套接字地址结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct in6_addr
{
uint8_t s6_addr[16]; // 128-bit IPv6 address
};
#define SIN6_LEN // required for compile-time tests
struct sockaddr_in6
{
uint8_t sin6_len; // length of this struct(28)
sa_family_t sin6_family; // AF_INET6
in_port_t sin6_port; // transport layer port# network byte ordered
uint32_t sin6_flowinfo; // flow information, undefined IPv6 address
struct in6_addr sin6_addr; // IPv6 address newtork byte ordered
uint32_t sin6_scope_id; // set of interfaces for a scope
};

如果系统支持套接字地址结构中的长度字段,那么 SIN6_LEN常值必须定义。

新的通用套接字地址结构

1
2
3
4
5
6
7
8
9
10
11
12
struct socketaddr_storage
{
uint8_t ss_len; // length of this struct (implementation dependent)
sa_family_t ss_family; // address family:AF_XXX value
/*
implementation-dependent elements to provide:
a) alignment sufficient to fulfill the alignment requirements of
all socket address types that the system supports
b) enough sotrage to hold anytype of socket address that the
system supports
*/
}

值-结果参数

1
2
3
4
5
struct sockaddr_un cli; // Unix domain
socklen_t len;

len = sizeof(cli); // len is a value
getpeername(unixfd, (struct sockaddr*)&cli, &len); // len may have changed

把套接字地质结构大小这个参数从一个整数改为指向某个整数变量的指针,其原因在于:当函数被调用时,结构大小时一个值,告诉内核该结构的大小,这样内核在写该结构时不会越界。当函数返回时,结构大小又是一个结果,它告诉进程内核在这个结构中存储了多少信息。这种类型的参数被称为值-结果参数

字节排序函数

1
2
3
4
5
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue); // 均返回:网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net16bitvalue); // 均返回:主机字节序的值

字节操纵函数

1
2
3
4
5
6
#include <stirngs.h>
void bzero(void *dest, size_t nbytes); // 把目标字节串中指定数目的字节置为0
void bcopy(const void *src, void *dest, size_t nbytes);
// 将指定数目的字节从源字节串转移到目标字节串
void bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
// 比较两个任意的字节串,相同则返回0, 否则返回非0
1
2
3
4
5
#include <string.h>
void *memset(void *dest, int c, size_t len); // 把目标字节串指定数目的字节置为c
void *memcpy(void *dest, void *src, size_t nbytes); // 同 bcopy
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
// 同bcmp,如果ptr1的字节大于ptr2,则返回值大于0,否则小于0

地址转换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <arpa/inet.h>
int inet_aton(const char*strptr, struct in_addr *addrptr);
// 将C字符串转换为32位网络字节序二进制值,成功返回1,否则返回0
in_addr_t inet_addr(const char *strptr);
// 与inet_aton进行相同的转换,返回值位32位网络字节序二进制值。
// 出错时该函数返回INADDR_NONE,通常位32位均为1的值,因此255.255.255.255不能由该函数处理。
char *inet_ntoa(struct in_addr inaddr);
// 将一个32位网络字节序二进制IPv4地址转换为相应的点分十进制数串。

// 头文件中定义
#define INET_ADDRSTRLEN 16
#define INET6_ADDR STRLEN 46
int inet_pton(int family, const char *strptr, void *addrptr);
// family参数可以是AF_INET,也可以是AF_INET6,成功返回1,输入不是有效的表达格式则为0,出错则为-1
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
// 进行相反的转换,成功则为指向结果的指针,出错则为NULL

基本TCP套接字编程

1
2
3
#include <sys/socket.h>
int socket(int family, int type, int protocol);
// 成功则返回非负描述符,出错则为-1
family常值
family 说明
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY 密钥套接字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); 
// 若成功则返回0,出错则返回-1
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
// 成功则为0, 出错则为-1
int lister(int sockfd, int backlog); // 若成功则为0, 出错则为-1
// 允许通过环境变量LISTENQ指定 backlog值的listen包裹函数
void Listen(int fd, int backlog)
{
char *ptr;
if ((ptr = getenv("LISTENQ")) != NULL)
backlog = atoi(ptr);
if (listen(fd, backlog) < 0)
err_sys("listen eror");
}
backlog

内核为每个监听套接字维护两个队列:

  • 未完成连接队列

    • 服务器收到了SYN分节,在等待完成相应的TCP三次握手过程,套接字处于SYN_RCVD状态
  • 已完成连接的队列

    • 服务器完成了三次握手,这些套接字处于ESTABLISHED状态

backlog曾被定义为两个队列总和的最大值。源自Berkeley的实现给backlog增设了一个模糊音字,把它乘以1.5得到未处理队列的最大长度。

1
2
3
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); 
// 成功则为非负描述符,出错则为-1
int close(int sockfd); // 若成功返回0,否则返回-1

并发服务器

1
2
3
4
5
6
7
8
#include <unistd.h>
pid_t fork(void); // 返回:在子进程中为0,在父进程中为子进程ID,出错则为-1
int execl(const char *pathname, const char *arg0, .../*(char *) 0 */);
int execv(const char *pathname, char *const *argv[]);
int execle(const char *pathnbame, const char *arg0, ... /* (char *) 0, chaar* const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, .../*(char *) 0 */);
int execvp(const char *filename, char *const argv[]);

fork只调用一次,在父进程中返回一次,在子进程中也返回一次。

可以用fork来创建进程自身的副本,也可以用fork创建一个进程的副本后调用exec函数把自身替换为新的程序

典型并发服务器程序的轮廓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pid_t pid;
int listenfd, connfd;
listenfd = socket(...);
bind(listenfd, ...);
listen(listenfd, LISTENQ);
for( ; ; )
{
connfd = Accept(listenfd, ...); //probably blocks
if ((pid = fork())== 0)
{
close(listenfd); // child closes listening socket
doit(connfd); // process the request
close(connfd); // done with this client;
exit(0); // child terminates
}
close(connfd); // parent close connected socket
}

getsockname 和 getpeername

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
// 返回与套接字关联的本地协议地址
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
// 返回与套接字关联的外地协议地址

// 例:获取套接字的地址族
int sockfd_to_family(int sockfd)
{
struct sockaddr_storage ss;
socklen_t len;
len = sizeof(ss);
if (getsockname(sockfd, (struct sockaddr*)&ss, &len) < 0)
return (-1);
return (ss.ss_family);
}