引言
尹圣雨的《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.基本交换流程
客户端只有在服务端调用listen后才能调用connect(可以在accept之前)
accept函数受理连接请求等待队列中待处理的客户端连接请求,函数调用成功时,其内部自动产生用于数据I/O的套接字,并返回其文件描述符。
2.迭代回声服务端/客户端
循环调用accept函数。
同一时刻只能服务于一个客户端。
判数据长度解决:操作系统可能把大数据分成多个数据包发送,客户端有可能在尚未收全时调用read
定义应用层协议表示数据边界
5.TCP原理
1.TCP套接字中的I/O缓冲
I/O缓冲特性:
I/O缓冲在每个TCP套接字中单独存在
I/O缓冲在创建套接字时自动生成
即使关闭套接字也会继续传递输出缓冲中遗留的数据
关闭套接字将丢失输入缓冲中的数据
由于TCP的数据流控制,不会发生超过输入缓冲大小的数据传输,不会因为缓冲溢出而丢失数据。
2.三次握手、传输数据、四次挥手
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分钟才可重新运行服务端。
套接字在四次挥手后并非立即消除,而是要经过一个Time-wait状态。只有先发送FIN消息的主机(不管是服务端还是客户端)才经过Time-wait状态,套接字处于Time-wait状态时,相应端口是正在使用状态,因此bind函数调用会报错。
注:一般无需考虑客户端的Time-wait状态,因为客户端套接字的端口是随机指定的,每次运行时会动态分配端口号,与服务端不同。
假设主机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算法
只有收到前一数据的ACK消息时,Nagle算法才发送下一数据。 其主要目的是减少网络流量,当你发送的数据包太小时,TCP并不立即发送该数据包,而是缓存起来直到数据包到达一定大小后才发送。
TCP套接字默认使用Nagle算法,因此最大限度地进行缓冲,直到收到ACK。
理解的关键的在于收到ACK消息有一定延时,在这个过程中”agle”依次进入缓冲区,收到ACK后一起发送。
5.可选项TCP_NODELAY
当网络流量不受太大影响时,不使用Nagle算法传输速度更快。典型如“传输大文件”,将文件数据传入输出缓冲不需要太多时间,因此,即使不使用Nagle算法,也会在装满输出缓冲时传输数据包。这不仅不会增加数据包的数量,反而会在无需等待ACK的前提下连续传输,因此可以大幅提高传输速度。
默认情况下,发送数据采用Nagle算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,将套接字可选项TCP_NODELAY置1可以禁止Nagle算法。