1 预备知识
1.1 IP地址
- IP协议有两个版本,分别是IPv4和IPv6。没有特殊说明,默认都是IPv4
- 对于IPv4,IP地址是一个四个字节32为的整数;对于IPv6来说,IP地址是128位的整数
-
我们通常也使用 “点分十进制” 的字符串表示IP地址,例如 180.101.50.172,用点分割的每一个数字表示一个字节,范围是 [0, 255] 。
-
公网IP:通常用来唯一地表示互联网中唯一的主机。
-
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
-
源 IP 和目的 IP:对一个报文来讲,回答了从哪里来到哪里去的问题,最大的意义是指导一个报文该如何进行路径选择。
1.2 端口号
- 端口号(port)是传输层协议的内容。它是一个 2 字节 16 位的整数,用来唯一地标识一台主机上的一个进程。
进程具有独立性,进程间通信的前提工作:先得让不同的进程看到同一份资源,这份资源在这里就是网络!
源端口号和目的端口号:描述数据是哪个进程发的,要发给哪个进程。
一个进程可以关联多个端口号,但是一个端口号不可以关联多个进程,这个可以由端口号的概念得出。
1.3 TCP协议和UDP协议
我们需要先对 TCP 协议和 UDP 协议有一个直观的认识,后面再详细讨论。
- TCP(Transmission Control Protocol,传输控制协议)
- 传输层协议。
- 有连接。
- 可靠传输。
- 面向字节流。
- UDP(User Datagram Protocol,用户数据报协议)
- 传输层协议。
- 无连接。
- 不可靠传输。
- 面向数据报。
说明:
- TCP 的可靠和 UDP 的不可靠都是中性词,客观的,没有谁好谁不好,只有谁更合适。
- 字节流:以字节为单位进行数据获取,可以根据需求进行数据量的获取,得到的数据需要自己解析才能得到相应的数据报。
- 数据报:以数据报为单位进行获取的,要么无法获取,要么获取到的数据就是一个完整的数据报,不需要上层的解析。
1.4 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分。网络数据流同样有大端和小端之分,那么为了避免网络通信中不同主机大小端不一致的问题,应如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
- 接收主机把从网络上接收到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
- 因此,网络数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。如果当前发送主机是小端机,就需要先将数据转成大端,否则就忽略,直接发送即可。
为了使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
注:这些函数名很好记,h 表示 host ,n 表示 network ,l 表示 32 位长整数,s 表示 16 位短整数。
举个例子:htonl
函数表示将 32 位的长整数从主机字节序转换为网络字节序。
2 socket 编程接口
2.0 socket 常见 API
网络通信的标准方式有很多种,比如基于 IP 的网络通信(它对应的通信协议家族是 AF_INET,网络套接字),还有原始套接字、域间套接字。有很多种类的套接字,其实就是编程接口。这几种编程接口都是各自不同的体系,于是就会有不同套的编程接口,这样就会很麻烦,因此,干脆把不同套的编程接口统一为同一套编程接口,也就是下面的这一套。换言之,要使用不同种类的通信方式,只需要改变传入的参数即可。
// 创建 socket 文件描述符 (客户端 + 服务器, TCP/UDP)
int socket(int domain, int type, int protocol);
// 绑定端口号 (服务器, TCP/UDP)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 设置socket文件状态为监听状态 (服务器, TCP)
int listen(int sockfd, int backlog);
// 接受连接 (服务器, TCP)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 发起连接 (客户端, TCP)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
于是,为了支持不同种类的通信方式,struct sockaddr
的结构就被设计出来了,它是一种通用结构。
struct sockaddr_in是用来网络的之间的进程通信,struct sockaddr_un是用来本地的进程间通信的。
socket API 的参数都用struct sockaddr *类型表示,在使用时传入各种类型的struct sockaddr指针,强转成struct sockaddr *即可。
这样,API 内部只要取得某种struct sockaddr的首地址,不需要知道具体类型,就可以根据地址类型字段确定结构体中的内容。这就相当于面向对象中的多态特性!
2.1 socket 系统调用
socket
的作用:为网络通信创建一个 socket 文件。
socket的参数:
① domain:指定协议家族。我们选择 AF_INET 。
② type:指定套接字类型。对于 TCP SOCK_STREAM ,应选择 ;对于 UDP ,应选择 SOCK_DGRAM 。
③ protocol:指定协议类型。在 TCP 和 UDP 中,我们设为 0 即可。
socket的返回值:
① 成功,返回一个 socket 文件描述符。
② 错误,返回 -1 。
2.2 bind 系统调用
bind的作用:将本地地址和一个 socket 文件进行绑定。
bind的参数:
① sockfd:传入 socket 文件描述符。
② addr:用于指定本端的 socket 信息。
③ addrlen:用于指定本端的 socket 信息的大小。
bind的返回值:
① 成功,返回 0 。
② 错误,返回 -1 。
2.3 recvfrom 系统调用
recvfrom的作用:从一个 socket 文件接收数据。
recvfrom的参数:
① sockfd:传入 socket 文件描述符。
② buf:用于存放读到的数据的用户层缓冲区。
③ len:用户层缓冲区的大小。
④ flags:读的方式。我们这里默认设为 0 即可。
⑤ src_addr:输入输出型参数,用于获取对端的 socket 信息。
⑥ addrlen:输入输出型参数,用于获取对端的 socket 信息的大小。
recvfrom的返回值:
① 成功,返回接收的字节数(当对端连接关闭时,返回 0)。
② 错误,返回 -1 。
2.4 sendto 系统调用
sendto的作用:从一个 socket 文件发送数据。
sendto的参数:
① sockfd:传入 socket 文件描述符。
② buf:用于发送数据的用户层缓冲区。
③ len:发送数据的长度。
④ flags:发送的方式。我们这里默认设为 0 即可。
⑤ dest_addr:目标对端的 socket 信息。
⑥ addrlen:目标对端的 socket 信息的大小。
sendto的返回值:
① 成功,返回发送的字节数。
② 错误,返回 -1 。
2.5 listen 系统调用
2.6 accept 系统调用
2.7 connect 系统调用