《TCP/IP网络编程》一、套接字/TCP/UDP

时间:2021-03-12 10:22:23

引言

尹圣雨的《TCP/IP网络编程》讲解清晰明了、循序渐进,作为入门读物值得一看。本文记录个人阅读中的摘要。
书中源码可在图灵社区下载

1.理解网络编程和套接字

1.服务端流程

调用socket函数创建套接字
调用bind函数分配IP地址和端口号
调用listen函数将套接字转为可接收连接状态
调用accept函数受理连接请求

2.客户端流程

调用socket函数创建套接字
调用connect函数向服务器端发送连接请求

3.文件描述符(文件句柄)

生成文件或套接字时,操作系统分配给它们的整数。Windows中称为句柄。
linux内部将套接字当做文件,但windows中区分文件句柄和套接字句柄,函数有区别。

2.套接字类型与协议设置

1.创建socket时的参数

协议族(PF_INET、PF_INET6),数据传输类型(SOCK_STREAM、SOCK_DGRAM),协议信息

2.面向连接的套接字SOCK_STREAM

传输过程中数据不会消失
按序传输数据
传输的数据不存在数据边界

3.面向消息的套接字SOCK_DGRAM

强调快速传输而非传输顺序
传输的数据可能丢失也可能损毁
传输的数据有数据边界
限制每次传输的数据大小

存在数据边界意味着接收数据的次数应和传输次数相同

3.地址族与数据序列

1.ipv4地址信息结构体sockaddr_in

struct sockaddr_in{
short sin_family; //地址族,一般为AF_INET,AF_XXX表示地址族、PF_XXX表示协议族(用于套接口创建)
u_short sin_port; //16位的IP端口
struct in_addr sin_addr; //32位IPv4地址结构
char sin_zero[8]; //8字节0值填充
};

2.通用地址结构sockaddr

必须强制指针地址转换struct sockaddr类型

struct sockaddr{
u_short sa_family; //地址族
char sa_data[14]; //14位目标地址
};

bind函数第二个参数期望得到sockaddr结构体,调用方法如下:(serv_addr为sockaddr_in类型)

bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))

3.字节序转换

网络字节序统一为大端序,小端系统传输数据时应转化为大端序排列方式
htons: 无符号短整型16位主机序转换为网络字节序
ntohs: 网络序的16位无符号整数(Port)转化为主机序
htonl、htohl 用于操作32位无符号长整型数(IP),作用同htons和ntohs
h指主机host字节序,n指网络network字节序,s指short,l指long
注:除了向sockadd_in结构体变量填充数据外,其他传输数据情况无需考虑字节序问题,自动完成。

4.inet_addr将ip地址点分十进制转换为4字节整数型

inet_aton功能相同,参数不同

5.网络地址初始化

    struct sockaddr_in serv_addr;
char * serv_ip="222.111.112.12";
char * serv_port="9190";
memset(&serv_addr, 0, sizeof(serv_addr));//结构体变量所有成员初始化为0
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(serv_ip);
//serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);//自动获取运行服务器端的计算机IP地址
serv_addr.sin_port=htons(atoi(argv[1]));

4.基于TCP的服务器/客户端

1.基本交换流程

《TCP/IP网络编程》一、套接字/TCP/UDP

客户端只有在服务端调用listen后才能调用connect(可以在accept之前)
accept函数受理连接请求等待队列中待处理的客户端连接请求,函数调用成功时,其内部自动产生用于数据I/O的套接字,并返回其文件描述符。

2.迭代回声服务端/客户端

循环调用accept函数。
同一时刻只能服务于一个客户端。
判数据长度解决:操作系统可能把大数据分成多个数据包发送,客户端有可能在尚未收全时调用read
定义应用层协议表示数据边界

5.TCP原理

1.TCP套接字中的I/O缓冲

《TCP/IP网络编程》一、套接字/TCP/UDP

I/O缓冲特性:
I/O缓冲在每个TCP套接字中单独存在
I/O缓冲在创建套接字时自动生成
即使关闭套接字也会继续传递输出缓冲中遗留的数据
关闭套接字将丢失输入缓冲中的数据

由于TCP的数据流控制,不会发生超过输入缓冲大小的数据传输,不会因为缓冲溢出而丢失数据。

2.三次握手、传输数据、四次挥手

《TCP/IP网络编程》一、套接字/TCP/UDP

6.UDP原理

1.TCP比UDP慢的原因

收发数据前后进行的连接设置及清除过程;收发数据过程中为保证可靠性而添加的流控制

2.UDP的高效使用

如果数据量小但需要频繁连接时,UDP比TCP更高效。有些特定场景要求:如传递压缩文件必须使用TCP,传递视频或音频可以使用UDP

3.UDP客户端套接字的地址分配

TCP客户端调用connect函数自动完成套接字地址分配;UDP调用sendto函数时自动分配IP和端口号

4.UDP是具有数据边界的协议

UDP传输中调用I/O函数的次数非常重要,输入和输出函数调用的次数应完全一致,才能保证接收全部已发送数据。

5.UDP套接字的已连接和未连接

针对UDP套接字调用connect函数并不意味着要与对方UDP套接字连接,只是想UDP套接字注册目标IP和端口信息,之后不仅可以使用sendto、recvfrom,还可以使用write、read函数进行通信。

7.基于TCP的半关闭

1.半关闭

linux的close函数和windows的closesocket函数意味着同时断开发送和接收;半关闭指只断开一部分连接,可以传输数据但无法接收,或者可以接收数据但无法传输。

2.针对优雅断开的shutdown函数

shutdown函数可以分别断开输入流、输出流和同时断开I/O流

3.为何需要半关闭

半关闭解决了服务端想客户端发送完数据,客户端断开连接前还有数据需要传递的情况。

8.域名及网络地址

1.为什么使用域名

服务器域名一般不常换,而IP地址相对改变频繁些;另外使用域名访问也更便利。

2.DNS服务器

如果默认DNS服务器无法解析主机询问的域名IP地址,则向上级DNS服务器询问,通过逐级向上传递信息,到达*DNS服务器——根DNS服务器,它知道该向哪个DNS服务器询问,向下传递解析请求,得到IP地址后原路返回。

3.域名和IP相互获取

#include<netdb.h>

struct hostent * gethostbyname(const char * hostname);

struct hostent * gethostbyaddr(const char * addr, socklen_t len, int family);

hostent结构体定义如下:

struct hostent
{
char * h_name;//官方域名
char ** h_aliases;//同一IP可以绑定多个域名
int h_addrtype;//地址族信息,IPv4则为AF_INET
int h_length;//IP地址长度,IPv4则为4
char ** h_addr_list;//以整数形式保存域名对应的(多个)IP地址
}

9.套接字的多种可选项

1.读取和设置可选项

#include<sys/socket.h>

int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);

int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);

2.Time-wait状态

服务端和客户端已建立连接的状态下,想服务端控制台输入ctrl+C,即强制关闭服务端,模拟服务端向客户端发送FIN消息。此时如果用同一端口号重新运行服务端,将输出“bind() error”消息,并且无法再次运行。再过大约3分钟才可重新运行服务端。
《TCP/IP网络编程》一、套接字/TCP/UDP
套接字在四次挥手后并非立即消除,而是要经过一个Time-wait状态。只有先发送FIN消息的主机(不管是服务端还是客户端)才经过Time-wait状态,套接字处于Time-wait状态时,相应端口是正在使用状态,因此bind函数调用会报错。
注:一般无需考虑客户端的Time-wait状态,因为客户端套接字的端口是随机指定的,每次运行时会动态分配端口号,与服务端不同。
《TCP/IP网络编程》一、套接字/TCP/UDP
假设主机A向主机B发送最后一条ACK消息后立即消除套接字,但这条ACK在传输过程中丢失,未能到达主机B。此时主机B会认为之前自己发送的FIN消息未能到达主机A,试图重传,但此时主机A已是完全终止状态,因此主机B永远无法收到主机A最后传来的ACK消息。相反,如果主机A处于Time-wait状态,则会重传ACK状态,使主机B正常关闭。

3.可选项SO_REUSEADDR

Time-wait状态不适用于某些必须尽快重启服务端以提供服务的情况,将套接字可选项中的SO_REUSEADDR状态改为1(默认值为0,无法分配Time-wait状态下的套接字端口号),可将Time-wait状态下的套接字端口号从新分配给新的套接字。

4.Nagle算法

《TCP/IP网络编程》一、套接字/TCP/UDP
只有收到前一数据的ACK消息时,Nagle算法才发送下一数据。 其主要目的是减少网络流量,当你发送的数据包太小时,TCP并不立即发送该数据包,而是缓存起来直到数据包到达一定大小后才发送。

TCP套接字默认使用Nagle算法,因此最大限度地进行缓冲,直到收到ACK。

理解的关键的在于收到ACK消息有一定延时,在这个过程中”agle”依次进入缓冲区,收到ACK后一起发送。

5.可选项TCP_NODELAY

当网络流量不受太大影响时,不使用Nagle算法传输速度更快。典型如“传输大文件”,将文件数据传入输出缓冲不需要太多时间,因此,即使不使用Nagle算法,也会在装满输出缓冲时传输数据包。这不仅不会增加数据包的数量,反而会在无需等待ACK的前提下连续传输,因此可以大幅提高传输速度。

默认情况下,发送数据采用Nagle算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,将套接字可选项TCP_NODELAY置1可以禁止Nagle算法。