二、Socket 编程
13. 什么是 Socket?
- Socket 本身有 “插座” 的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型
- 本质为内核借助缓冲区形成的伪文件
- 既然是文件,那么可以使用文件描述符引用套接字
- 与管道类似,Linux 系统将其封装成文件是为了统一接口,使得读写套接字和读写文件的操作一致
- 区别是:管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递
- 在 TCP/IP 协议中,“IP 地址 + TCP 或 UDP 端口号” 唯一标识网络通信中的一个进程
- “IP 地址 + 端口号” 就对应一个 socket
- 欲建立连接的两个进程各自有一个 socket 来标识,那么这两个 socket 组成的 socket pair 就唯一标识一个连接,因此可以用 socket 来描述网络连接的一对一关系
- 在网络通信中,套接字一定是成对出现的
- 一个文件描述符指向一个套接字 (该套接字内部由内核借助两个缓冲区实现)
14. 网络字节序
- 内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节,而发送主机可能是小端字节序,也可能是大端字节序。为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换(PC 机采用小端法,网络采用大端法)
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
#include <arpa/inet.h> // h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数 uint32_t htonl(uint32_t hostlong); // 本地转网络(IP) uint16_t htons(uint16_t hostshort); // 本地转网络(port) uint32_t ntohl(uint32_t netlong); // 网络转本地(IP) uint16_t ntohs(uint16_t netshort); // 网络转本地(port)
15. Socket 模型创建流程
- 1 个客户端和 1 个服务器端进行通信:一共有 3 个套接字(一对用于通信的套接字,服务器单独还有一个用于监听的套接字)
15.1 socket 函数
// 创建一个套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
-
domain
- AF_INET 这是大多数用来产生 socket 的协议,使用 TCP 或 UDP 来传输,用 IPv4 的地址
- AF_INET6 与上面类似,不过是用 IPv6 的地址
-
type
- SOCK_STREAM(流式) 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的 socket 类型,这个 socket 使用 TCP 来进行传输
- SOCK_DGRAM(报式) 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用 UDP 来进行它的连接
-
protocol
- 传 0 表示使用默认协议
-
返回值
- 成功:返回新套接字(socket)对应的文件描述符
- 失败:返回 -1,设置 errno
15.2 bind 函数
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用 bind 绑定一个固定的网络地址(IP)和端口号(port)到套接字
-
sockfd
- 指定待绑定的 socket(用于监听的套接字) 文件描述符
-
addr
- 构造出 IP 地址加端口号
- 类型是 struct sockaddr *
/* struct sockaddr_in { sa_family_t sin_family; // address family: AF_INET in_port_t sin_port; // port in network byte order struct in_addr sin_addr; // internet address }; // internet address struct in_addr { uint32_t s_addr; // address in network byte order }; */ struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8888); // 因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听 // 直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址 addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY 宏:表示取出系统中有效的任意 IP 地址,二进制类 (struct sockaddr *)&addr // addr 类型为 struct sockaddr*,故此处需要强制转换
- addrlen
- sizeof(addr) 结构体长度
- 返回值
- 成功:返回 0
- 失败:返回 -1, 设置 errno
15.3 listen 函数
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
-
listen 函数用于设置同时与服务器建立连接的(监听)上限数 (同时进行 3 次握手的客户端数量)
-
sockfd
- socket 对应的文件描述符
-
backlog
- 排队建立 3 次握手队列和刚刚建立 3 次握手队列的链接数和
- 最大值 128
-
典型的服务器程序可以同时服务于多个客户端
- 当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接
- 如果有大量的客户端发起连接而服务器来不及处理,尚未 accept 的客户端就处于连接等待状态
- listen() 声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接等待状态,如果接收到更多的连接请求就忽略
- listen() 成功返回 0,失败返回 -1
15.4 accept 函数
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- accept 函数用于阻塞(监听)等待客户端建立连接
- 三方握手完成后,服务器调用 accept() 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
- sockdf
- socket 文件描述符,socket 函数返回值
- addr
- 传出参数,返回成功与服务器建立连接的那个客户端的地址结构(含 IP 地址和端口号)
用于和客户端建立连接的套接字
- 传出参数,返回成功与服务器建立连接的那个客户端的地址结构(含 IP 地址和端口号)
- addrlen
- 传入传出参数
- 传入:sizeof(addr) 大小
- 传出:客户端 addr 实际大小
- 传入传出参数
- 返回值
- 成功:返回一个新的用于和客户端通信的 socket 对应的文件描述符
- 失败:返回 -1,设置 errno
15.5 connect 函数
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- connect 函数作用:使用现有的 socket 与服务器建立连接
- connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址
- 如果不使用 bind 绑定客户端地址结构,则系统默认采用 “隐式绑定”
- sockdf
- socket 文件描述符
- addr
- 传入参数,指定服务器端地址结构(含 IP 地址和端口号)
- addrlen
- 传入参数,传入 sizeof(addr) 服务器端地址结构的长度
- 返回值
- 成功:返回 0
- 失败:返回 -1,设置 errno