Procházet zdrojové kódy

feat: TCP连接的一些函数

刘聪 před 3 roky
rodič
revize
c2876ab312
7 změnil soubory, kde provedl 502 přidání a 0 odebrání
  1. binární
      cpp/img/Net_10.png
  2. binární
      cpp/img/Net_11.png
  3. binární
      cpp/img/Net_12.png
  4. binární
      cpp/img/Net_13.png
  5. binární
      cpp/img/Net_8.png
  6. binární
      cpp/img/Net_9.png
  7. 502 0
      cpp/网络编程.md

binární
cpp/img/Net_10.png


binární
cpp/img/Net_11.png


binární
cpp/img/Net_12.png


binární
cpp/img/Net_13.png


binární
cpp/img/Net_8.png


binární
cpp/img/Net_9.png


+ 502 - 0
cpp/网络编程.md

@@ -334,3 +334,505 @@ MSS(最大分段大小)限制通过网络(例如互联网)传输的数
 
 #### TIME_WAIT状态
 
+
+## 基本套接字
+
+### 套接字地址结构
+
+#### IPv4套接字地址结构
+
+大多数套接字函数都需要一个指向套接字地址结构的指针作为参数
+
+每个协议族都定义它自己的套接字地址结构,这些结构的名字均以`sockaddr_`开头,并以对应每个协议族的唯一后缀结尾
+
+```cpp
+struct in_addr{
+	in_addr_t s_addr;			// 32-bit IPv4 address
+};
+
+struct sockaddr_in {
+	uint8_t			sin_len;			// length of structure(16)
+	sa_family_t   	sin_family;         //address family
+	in_port_t 		sin_port;           //16 bit TCP/UDP port number
+	struct  in_addr sin_addr;   		//32 bit IP address
+	char    		sin_zero[8];        //not use, for align
+};
+```
+
+> `sin_len`长度字段是为了增加对OSI协议的支持而增加的,在此之前第一个成员是`sin_family`也就是说不是所有厂家都支持`sin_len`字段,而且POSIX规范也不要求这个成员
+
+- 即使有`sin_len`长度字段,也无需设置和检查它,触发涉及路由套接字。它是由处理来自不同协议族的套接字地址结构的例程在内核中使用的
+
+> 在源自`Berkeley`的实现中,从进程到内核传递套接字地址结构的4个套接字函数(`bind`,`connet`,`sendto`和`sendmsg`)打偶要调用`sockargs`函数,该函数从进程复制套接字地址结构,并显式地把它的`sin_len`字段设置成早先作为参数传递给这个4个函数的该地址结构的长度
+
+> 从内核到进程传递套接字地址结构的5个套接字函数分别是`accept`,`recefrom`,`recvmsg`,`getpeername`和`getsockname`,均在返回进程之前设置`sin_len`字段
+
+- POSIX规范只需要`sin_family`,`sin_addr`和`sin_port`三个字段。对于符合POSIX的实现来说,定义额外的结构字段是可以接受的,这队伍网际套接字地址结构来说也是正常的。几乎所有的试下你都增加了`sin_zero`字段,所以所有的套接字地质结构大小都至少是16字节
+
+- `struct in_addr`数据类型必须是一个至少32位无符号整数类型,`in_port_t`必须是一个至少16位的无符号整数类型,`sa_family_t`可以是任何无符号整数类型
+
+> 在支持长度字段的实现中,`sa_family_t`通常是一个8位的无符号整数;在不支持长度字段的实现中,它一般是16位无符号整数
+
+| 数据类型 | 说明 | 头文件 |
+| --- | --- | --- |
+| int8_t | 带符号的8位整数 | `<sys/types.h>` |
+| uint8_t | 不带符号的8位整数 | `<sys/types.h>` |
+| int16_t | 带符号的16位整数 | `<sys/types.h>` |
+| uint16_t | 不带符号的16位整数 | `<sys/types.h>` |
+| int32_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>` |
+
+- IPv4地址和TCP或UDP开端口号在套接字地址结构中总是以**网络字节序**来存储
+- 32位IPv4地址存在两种不同的访问方法,所以必须正确使用IPv4地址,尤其是在它作为函数的参数时,因为编译器对**传递结构**和**传递整数**的处理时完全不同的
+- `sin_zero`字段未曾使用,一般把整个结构置为0,而不是单单把`sin_zero`置0
+- 套接字地址结构仅在给定主机上使用。虽然结构中的某些字段用在不同主机之间的通信中,但是结构本身并不在主机之间传递
+
+#### 通用套接字地址结构
+
+当作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式(指向该结构的指针)来传递。但是以这样的**指针作为参数**之一的任何套接字函数必须处理来自所支持的**任何协议族**的套接字地址结构
+
+`void*`类型是在套接字函数定义之后出现的,所以当时的为了解决适配所有协议族的套接字函数,在`<sys/socket.h>`中定义了`sockaddr`**通用套接字地址结构**
+
+```cpp
+struct sockaddr {
+	__uint8_t       sa_len;         /* total length */   
+	sa_family_t     sa_family;      /* [XSI] address family */
+	char            sa_data[14];    /* [XSI] addr value (actually larger) */
+};
+```
+
+套接字函数被定义为指向某个通用套接字地址结构的一个指针作为其参数之一
+
+```cpp
+int bind(int, struct sockaddr*, socklen_t);
+```
+
+> `bind`将`sockaddr*`作为参数
+
+这就要求这些函数的任何调用都必须要指向特定于协议的套机子地址结构的指针进行强制类型转换
+
+```cpp
+struct sockaddr_in serv;
+bind(sockfd, (struct sockaddr*)&serv, sizeof(serv));
+```
+
+#### IPv6套接字地址结构
+
+```cpp
+struct sockaddr_in6
+{
+	uint8_t			sin6_len;			
+	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_socpe_id;  /* scope ID, 尚处于实验阶段 */
+};
+
+struct in6_addr
+{
+	unsigned char sa_addr[16]; 	/* ipv6地址, 要使用网络字节序表示 */
+};
+```
+
+> `<netinet/in.h>`中定义
+
+- 如果系统支持套接字地址结构中的长度字段`sin6_len`,那么`sin6_len`必须定于
+- `IPv6`的地址族时`AF_INET6`,`IPv4`的地址族时`AF_INET`
+- 结构中字段的先后顺序做过编排,是的如果`sockaddr_in6`结构本身是64位对其的,那么128位的`sin6_addr`字段也是64位对其的
+- `sin6_flowinfo`字段分成两个字段
+  - 低序20位是流标
+  - 高序12位保留
+- 对于具备范围的地址(scoped address),`sin6_scope_id`字段标识其范围,最常见的是链路局部地址(link-local address)的接口索引(interface index)
+
+#### 新的通用套接字地址结构
+
+作为IPv6套接字API的一部分而定义的新的通用套机子地址结构克服了现有的`struct sockaddr`的一些缺点,新的`struct sockaddr_storage`足以容纳系统所支持的任何套接字地质结构
+
+```cpp
+struct sockaddr_storage {
+    uint8_t ss_len;
+    sa_family_t ss_family;
+    char ss_padding[SIZE];
+}
+```
+
+- 与`sockaddr`存在的区别
+  - 系统支持的任何套接字地址结构有对其需要,`sockaddr_storage`都能够满足
+  - `sockaddr_storage`足够大,容纳系统支持的任何套接字地址结构
+
+- 注意
+  - `sockaddr_storage`中除了`ss_family`和`ss_len`之外的其他字段,对用户同某个
+  - `sockaddr_storage`结构必须强制类型转换成或复制到适合于`ss_family`字段所给出地址类型的套接字地址结构中,才能访问其他字段
+
+#### 套接字地址结构的比较
+
+![TCP链接的分组交换](./img/Net_8.png)
+
+### 值-结果参数
+
+套接字函数传递一个套接字地址结构时,总是以指针的形式传递,该结构的长度作为另一个参数来传递,不过该传递分两种:内核到进程、进程到内核
+
+1. 从进程到内核传递套接字地址结构的函数有3个:`bind`,`connect`和`sendto`,这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小
+
+```cpp
+int send(
+  _In_       SOCKET s,
+  _In_ const char   *buf,
+  _In_       int    len,
+  _In_       int    flags
+);
+
+int connect(
+  _In_ SOCKET                s,
+  _In_ const struct sockaddr *name,
+  _In_ int                   namelen
+);
+```
+
+指针和指针所指内容的大小都传递给了内核,于是内核知道需要从进程复制多少数据进来
+
+![TCP链接的分组交换](./img/Net_9.png)
+
+2. 从内核到进程传递套接字地质结构的函数有4个:`accept`,`recvfrom`,`getsockname`和`getpeername`
+
+这些函数的一个参数指向某个套接字地址结构的指针,另一个表示指向该结构带线啊哦的指数变量的指针
+
+```cpp
+int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
+ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags, struct sockaddr *restrict address, socklen_t *restrict address_len);
+int	getsockname(int, struct sockaddr *restrict, socklen_t *restrict);
+int getpeername(int, struct sockaddr *restrict, socklen_t *restrict);
+```
+
+![TCP链接的分组交换](./img/Net_10.png)
+
+把套接字地址结构大小这个参数从一个整数改为指向某个整数变量的指针,是为了
+
+1. 当时被调用时,结构大小是一个值,它告诉内核该结构的大小,这样内核在写该结构时不至于越界
+2. 当函数返回时,结构大小是一个结果,它告诉进程内核在结构中究竟存储了多少信息
+
+使用值-结果参数作为套接字地址结构的长度时
+1. 如果套接字地址结构是固定长度(sockaddr_in 长度是16,sockaddr_in6长度是28),那么内核返回的长度也是那个固定值
+2. 如果是可变长度的套接字地址结构(Unix域的sockaddr_un),内核返回的长度可能是小于该结构的最大长度
+
+### 字节排序函数
+
+一个16位的整数,由两个字节组成,内存中存储这两个字节有两种方法
+1. 低序(就是权值较小的后面那几位)字节存储在起始地址,称为**小端字节序**
+2. 高序字节存储在起始地址,称为**大端字节序**
+
+![TCP链接的分组交换](./img/Net_11.png)
+
+遗憾的是,这两种字节序之间没有标准可循,两种格式都有系统使用
+
+```cpp
+union {
+	short s;
+	char c[sizeof(short)];
+} un;
+un.s = 0x0102;
+std::cout << (int)un.c[0] << " " << (int)un.c[1] << std::endl;
+```
+
+> 输出`1 2`是大端,输出`2 1`是小端
+
+![TCP链接的分组交换](./img/Net_12.png)
+
+一般把某个给定系统所用的字节序称为**主机字节序**
+
+网络协议指定的字节序称为**网络字节序**
+
+在每个TCP分节中都有16位端口号和32位的IPv4地址。发送协议栈和接收协议栈必须就这些多字节字段和各个字节的传送顺序达成一致。**网际协议使用大端字节序**来传送这些多字节整数
+
+> 网络使用大端的一种解释:先收到校验信息,提前预处理
+
+理论上,按主机字节序存储套接字地质结构中的各个字段,等到需要时再在主机字节序和网络字节序之间进行自动转换,使用者无需关注细节。但是历史原因,套接字地址结构中的某些字段必须按照网络字节序进行维护
+
+因此,需要关注如何在主机字节序和网络字节序之间相互转换
+
+```cpp
+#include <arpa/inet.h>
+
+// 均返回网络字节序的值
+uint32_t htonl(uint32_t hostlong);
+uint16_t htons(uint16_t hostshort);
+
+// 均返回主机字节序的值
+uint32_t ntohl(uint32_t netlong);
+uint16_t ntohs(uint16_t netshort);
+```
+
+> h表示`host`主机,n表示`network`网络,s表示`short`,l表示`long`
+
+使用这些函数时,不关系主机字节序和网络字节序的真实值,只用调用合适的函数在主机和网络字节序之间转换某个给定值
+
+### 字节操纵函数
+
+以空字符结尾的C字符串是由在`<string.h>`头文件中定义、名字以str开头的函数处理  
+
+名字以mem开头的函数用于处理内存
+
+名字以b开头的函数用于处理字节
+
+```cpp
+#include <strings.h>
+
+void bzero(void *s, size_t n); 
+void bcopy(const void *s1, void *s2, size_t n);
+int bcmp(const void *s1, const void *s2, size_t n);	// 若相等 返回0,否则返回非0
+```
+
+`bzero`把目标字节串中指定数据的字节置为0,经常用来把一个套接字地址结构初始化0
+
+`bcopy`将指定数目的字节从源字节串移到目标字节串
+
+`bcmp`用于比较两个字节串
+
+```cpp
+#include <string.h>
+
+void *memcpy(void *restrict s1, const void *restrict s2, size_t n);
+void *memcpy(void *restrict s1, const void *restrict s2, size_t n);
+int memcmp(const void *s1, const void *s2, size_t n);
+```
+
+`memset`把目标字节串指定数目的字节置为值C
+
+`memcpy`类似`bcopy`不过两个指针参数的顺序相反
+
+`memcmp`比较两个任意的字节串,相同返回0,否则返回非0
+
+### inet_aton、inet_addr、inet_ntoa函数
+
+用于ASCII字符串域网络字节序的二进制之间转换网际地址
+
+1. `inet_aton`、`inet_addr`、`inet_ntoa`在点分十进制数串(例如`206.140.23.123`)与它长度为32位的网络字节序二进制值之间转换IPv4地址
+2. `inet_pton`和`inet_ntop`对于IPv4和Ipv6都适用
+
+```cpp
+#include <arpa/inet.h>
+
+int inet_aton(const char *string, struct in_addr*addr);		// 若字符串有效 返回1 否则返回0
+in_addr_t inet_addr(const char *cp);						// 若字符串有效 返回32位二进制网络字节序IPv4地址,否则返回INADDR_NONE
+char *inet_ntoa(struct in_addr in);							// 返回点分十进制数串的指针
+```
+
+> inet_addr返回值INADDR_NONE是每个位全为1的常值,也就是255.255.255.255,那么对于IPv4的广播地址就无法使用inet_addr表示,所以应该**尽可能不用**`inet_addr`
+
+### inet_pton和inet_ntop函数
+
+这两个函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用
+
+函数中p和n分别代表表达(presentation)和数值(numeric)
+
+```cpp
+#include <arpe/inet.h>
+int inet_pton(int family, const char *strptr, void *addrptr);   //将点分十进制的ip地址转化为用于网络传输的数值格式
+// 返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
+ 
+const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);     //将数值格式转化为点分十进制的ip地址格式
+// 返回值:若成功则为指向结构的指针,若出错则为NULL
+```
+
+这两个函数的`family`参数既可以是`AF_INET`,也可以是`AF_INET6`,如果不被不支持的地址族作为family,会返回一个错误,毕竟`errno`置为`EAFNOSUPPORT`
+
+`inet_ntop`中的len表示目标存储单元的大小(strptr),一面该函数溢出其调用者的缓冲区。下面的宏定义有助于指定大小。
+
+```cpp
+#define INET_ADDRSTRLEN 	16
+#define INET6_ADDRSTRLEN 	46
+```
+
+如果len太小,不足与容纳表达式格式(包括串结束符),会返回**空指针**,并置errno位`ENOSPC`
+
+`inet_ntop`函数的`strptr`不可为空指针,调用者必须为目标存储单元分配内存并指定其大小
+
+### readn、writen和readline函数
+
+字节流套接字上的`read`和`write`函数所表现的行为不同于通常的文件IO
+
+字节流套接字上调用`read`或`write`输入或输出的字节数可能比请求的数量少,因为内核中用于套接字的缓冲区可能已经达到了极限。此时,所需的是调用者再次调用`read`或`write`函数,以输入或输出剩余的字节
+
+> 这种现象在read一个字节流套接字时很常见,但是在write一个字节流套接字时只能在该套接字为非阻塞的前提下才出现
+
+为了以防万一,使用`writen`函数来替代`write`函数
+
+```cpp
+ssize_t writen (int fd, const void *buf, size_t num)
+{
+	ssize_t res;
+	size_t n;
+	const char *ptr;
+	
+	n = num;
+	ptr = buf;
+	while (n > 0) {
+	/* 开始写*/ 
+		if ((res = write (fd, ptr, n)) <= 0) 
+		{
+			if (errno == EINTR)
+				res = 0;
+			else
+				return (-1);
+		}
+	
+		ptr += res;/* 从剩下的地方继续写     */ 
+		n -= res;
+	}
+	return (num);
+}
+
+ssize_t readn (int fd, void *buf, size_t num)
+{
+	ssize_t res;
+	size_t n;
+	char *ptr;
+	
+	n = num;
+	ptr = buf;
+	while (n > 0) {
+		if ((res = read (fd, ptr, n)) == -1) 
+		{
+			if (errno == EINTR)
+				res = 0;
+			else
+				return (-1);
+		}
+		else if (res == 0)
+			break;
+	
+		ptr += res;
+		n -= res;
+	}
+	
+	return (num - n);
+}
+
+ssize_t readline(int fd, void *vptr, size_t maxlen)
+{
+	ssize_t	n, rc;
+	char	c, *ptr;
+ 
+	ptr = vptr;
+	for (n = 1; n < maxlen; n++) {
+        again:
+		if ( (rc = read(fd, &c, 1)) == 1) {
+			*ptr++ = c;
+			if (c == 'n')
+				break;	/* newline is stored, like fgets() */
+		} else if (rc == 0) {
+			*ptr = 0;
+			return(n - 1);	/* EOF, n - 1 bytes were read */
+		} else {
+			if (errno == EINTR)
+				goto again;
+			return(-1);		/* error, errno set by read() */
+		}
+	}
+ 
+	*ptr = 0;	/* null terminate like fgets() */
+	return(n);
+}
+
+```
+
+## 基本TCP套接字编程
+
+![基本TCP客户端、服务器程序的套接字函数](./img/Net_13.png)
+
+### socket函数
+
+为了执行网络IO,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型
+
+```cpp
+#include <sys/socket.h>
+
+int socket(int family, int type, int protocol);		// 成功返回非负描述符,否则返回-1
+```
+
+`family`用于指明协议族,也往往被称为协议域;`type`指明套接字类型;`protocol`用于设定某个协议类型的常值
+
+| family | 说明 |
+| --- | --- |
+| AF_INET | IPv4协议 |
+| AF_INET6 | IPv6协议 |
+| AF_LOCAL | Unix域协议 |
+| AF_ROUTE | 路由套接字 |
+| AF_KEY | 密钥套接字 |
+
+> family协议族的一些常值
+
+| type | 说明 |
+| --- | --- |
+| SOCK_STREAM | 字节流套接字,提供面向连接的稳定数据传输,即TCP协议 |
+| SOCK_DGRAM | 数据报套接字,使用不连续不可靠的数据包连接 |
+| SOCK_SEQPACKET | 有序分组套接字,提供连续可靠的数据包连接 |
+| SOCK_RAW | 原始套接字,提供原始网络协议存取 |
+| SOCK_RDM | 提供可靠的数据包连接 |
+| SOCK_PACKET | 与网络驱动程序直接通信 |
+
+> type套接字类型的一些常值
+
+| protocol | 说明 |
+| --- | --- |
+| IPPROTO_TCP | TCP传输协议 |
+| IPPROTO_UDP | UDP传输协议 |
+| IPPROTO_SCTP | SCTP传输协议 |
+
+> protocol的一些类型常值,**或者设置为0**
+
+| | AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY |
+| --- | --- | --- | --- | --- | --- |
+| SOCK_STREAM | TCP、SCTP | TCP、SCTP | 是 | | |
+| SOCK_DGRAM | UDP | UDP | 是 | | |
+| SOCK_SEQPACKET | SCTP | SCTP | 是 | | |
+| SOCK_RAW | IPv4 | IPv6 | | 是 | 是 |
+
+并非所有的family与type的组合都是有效的,上面列表中空白格表示无效,其余都是有效
+
+`socket()`函数在成功之后返回一个小的非负整数,它与文件描述符类似,一般称之为**套接字描述符**,也叫`sockdf`
+
+### connet函数
+
+```cpp
+#include <sys/socket.h>
+
+int connect(int socket, const struct sockaddr *address, socklen_t address_len);
+```
+
+TCP客户端用connect函数来建立与TCP服务器的连接
+
+`socket`参数是用`socket()`函数返回的套接字描述符,`address`和`address_len`分别是一个指向套接字地址结构的指针和该结构的大小
+
+> 客户在调用`connect`之前并不是非得调用`bind`函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口
+
+如果是TCP套接字,调用`connect`函数会激发TCP的三次握手,而且仅在连接建立成功或出错时才返回,其中出错可能有几种情况
+
+1. TCP客户端没有收到SYN分节的响应,则返回ETIMEDOUT错误
+2. 若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(服务器进程没有运行也有可能)。这是一种硬错误(hard error),客户端一接收到RST就马上返回ECONNREFUSED错误
+	- RST产生的条件1:目的地为某端口的SYN到达,然而该端口没有正在监听的服务器
+	- RST产生的条件2:TCP想取消一个已有连接
+	- RST产生的条件3:TCP接收到一个根本不存在的连接上的分节
+3. 客户发出的SYN在中间的某个路由器上引发`destination unreachable`目的地不可达的ICMP错误,则认为是一种软错误(soft error)
+
+### bind函数
+
+bind函数把一个本地协议地址赋予一个套接字,对于网际网协议,协议地址是32位IPv4地址或128位Ipv6地址与16位的TCP或UDP端口号的组合
+
+```cpp
+#include <sys/socket.h>
+
+int bind(int socket, const struct sockaddr *address, socklen_t address_len);
+```
+
+`socket`参数是用`socket()`函数返回的套接字描述符,`address`和`address_len`分别是一个指向套接字地址结构的指针和该结构的大小
+
+对于TCP,`bind`函数可以指定一个端口号,或者指定IP地址,也可以两个都指定或都不指定
+
+如果TCP客户端或服务器未曾调用`bind`函数绑定端口,当调用`connect`或`listen`时,内核会临时分配端口。让内核选择临时端口对TCP客户端来说正常,但是**服务器的端口必须指定**,因为服务器端口需要外部知道才能主动连接
+