最近学习了一下socket
编程,之前只会调用 api 按照书上/网上的方法按部就班地实现,这次集中梳理一下socket
编程中一些关键的概念,以及如何去理解这些概念。
1 socket 类型
socket 方法是一种 IPC(inter process communication)方法
socket 除了常见的同一主机(AF_UNIX
)和网络上的主机(AF_INET/AF_INET6
)交换信息外,还包含了许多其他类型:
/* Address families. */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6
#define AF_ROSE PF_ROSE
#define AF_DECnet PF_DECnet
#define AF_NETBEUI PF_NETBEUI
#define AF_SECURITY PF_SECURITY
#define AF_KEY PF_KEY
#define AF_NETLINK PF_NETLINK
#define AF_ROUTE PF_ROUTE
#define AF_PACKET PF_PACKET
#define AF_ASH PF_ASH
#define AF_ECONET PF_ECONET
#define AF_ATMSVC PF_ATMSVC
#define AF_RDS PF_RDS
#define AF_SNA PF_SNA
#define AF_IRDA PF_IRDA
#define AF_PPPOX PF_PPPOX
#define AF_WANPIPE PF_WANPIPE
#define AF_LLC PF_LLC
#define AF_IB PF_IB
#define AF_MPLS PF_MPLS
#define AF_CAN PF_CAN
#define AF_TIPC PF_TIPC
#define AF_BLUETOOTH PF_BLUETOOTH
#define AF_IUCV PF_IUCV
#define AF_RXRPC PF_RXRPC
#define AF_ISDN PF_ISDN
#define AF_PHONET PF_PHONET
#define AF_IEEE802154 PF_IEEE802154
#define AF_CAIF PF_CAIF
#define AF_ALG PF_ALG
#define AF_NFC PF_NFC
#define AF_VSOCK PF_VSOCK
#define AF_KCM PF_KCM
#define AF_QIPCRTR PF_QIPCRTR
#define AF_SMC PF_SMC
#define AF_MAX PF_MAX
事实上除了少数的几个协议族使用频率较高之外,其他的协议族比较少见。其中QEMU
虚拟CAN
设备时就使用了AF_CAN
协议族。
2 socketfd、socketaddr 和 addrinfo
socketfd
可理解为socket
在内核中的具象化体现,它本质上是一个文件描述符,存在于内核空间中,用户无法直接看到。而对于一个普通的文件描述符(fd)来说,用户可以直观地看到文件(或者说路径 path),对应文件的路径,socket
对用户暴露出来的是socketaddr
。
对于不同的socket
来说,使用的socketaddr
是不同的。AF_UNIX
使用一个路径名,AF_INTE/AF_INTE6
使用 IP 地址和端口。
AF_INTE/AF_INTE6
使用的 IP 地址和端口在内核中可用addrinfo
来表示。使用getaddrinfo
方法可以获取一个socketaddr
,使用getnameinfo
方法可以反过来获取对应的 IP 地址和端口。
转换关系如图:
3 socket 所在网络层次
4 并发型服务器设计方案
并发型服务器设计方案有很多
- 采用 fork 进程的方式,每当有一个客户端连接到服务端,则开启一个子进程专门处理该客户端请求;
- 采用线程池或进程池的方式,方案 1 的进阶版,该方案的要点在于,提前建立多个子进程(子线程),客户端连接时,子进程(子线程)处理客户端请求,客户端请求结束后,子进程(子线程)不终止,等待下一个待处理的客户端请求;
- 采用 I/O 多路复用的方式, 在单一进程中通过同时监控多个文件描述符(socketfd 也是文件描述符)上的 I/O 事件的模型(I/O 多路复用、型号驱动 I/O 或者 epoll)。
5 SOCKET TCP 粘包问题
在 tcp 开发中最容易遇到的问题就是粘包问题。
在我写的客户端中,进行了频繁的 send 操作,服务端中循环 recv,但是在服务端中 recv 到的数据却总是缺几条,并且缺失的数量和位置都不固定。代码如下:
1 |
|
1 |
|
server 端的结果可能为:
buf: hello1
buf: hello4
buf: hello6
注意,在这里缺失的数量和位置都是不固定的,但是,如果在 send 前面添加 sleep,则 sleep 后一条必定可打印
1 |
|
则 server 端打印中必有
1 |
|
其他项仍不固定。
在这里要明确一个关键的概念:
TCP 是面向流的协议,它发送的不是数据包。
之所以出现上面的问题,就是因为我们把 TCP 理解成发送数据包,误以为 send 和 recv 是一一对应的,send 发送一个数据包,recv 接收一个数据包。这是错误的理解!
事实上,在内核的 TCP 机制中,TCP 拥有两个缓冲区,一个用来发送,一个用来接收。
-
客户端调用 send 时,若数据成功发送到缓冲区(若缓冲区满则阻塞),send 返回,应用程序认为数据已经成功发送。此时,数据交由内核处理,应用程序不再过问。内核根据 TCP 协议的机制,判断何时将缓冲区内容打包发送到服务端。
-
服务端调用 recv,将内核接收缓冲区中的内容读入,读取后,应用程序解包数据。
注意:频繁调用 send,或网络通信质量不佳,会导致客户端往发送缓冲区填写多个数据(多个 hello),服务端调用 recv 一次性读入 BUF_SIZE 大小的数据,导致一次性读入多个数据包(多个 hello),这就是粘包。
修改程序,打印 numRead 和 numWrite 值。调试 recv,查看 buf 内容:
"hello1/000hello2/000/hello3/000"
buf 内容证明了上面的猜测,由于’/000’导致 printf 只打印了第一个字符串,才出现这样的结果。