欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-22 18:42:46
...

引言

尹圣雨的《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算法。

相关标签: 网络编程 socket