Socket
Socket基础
在Unix/Linux系统中,Socket 也被视为一种文件。所以基本的编程方法也是:打开-读写-关闭。
使用socket()
函数创建一个新的 Socket,类似于打开文件,返回一个整数类型的文件描述符。例如:
- AF_INET
:使用IPv4协议
- SOCK_DGRAM
:使用数据报
- SOCK_STREAM
:使用数据流
- 0
:使用默认协议,对于数据报来说是UDP,对于数据流来说是TCP。
那么如何写呢?还是以UDP协议为例。根据我们对UDP协议的认识,我们只需要指定发送的IP和PORT,就可以发送数据包了,不需要建立连接。调用sendto
方法。
bool sendTo(const std::string& data, const std::string& ip, uint16_t port) {
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(port);
dest_addr.sin_addr.s_addr = inet_addr(ip.c_str());
ssize_t sent = sendto(socket_fd_, data.c_str(), data.length(), 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr));
return sent > 0;
}
那么如何读呢?对于UDP协议来说,只需要指定本机的端口(bind
),然后调用recvfrom
即可。
一个更全面的读写方法总结如下:
Reading | Writing | Description |
---|---|---|
read |
write |
在一个连续的buffer上进行读/写 |
readv |
writev |
在多个buffer上进行读/写 |
recv |
send |
相比于 read & write ,有更多的控制 flag |
recvfrom |
sendto |
同时获取/指定远程地址的读/写(针对基于包的协议) |
recvmsg |
sendmsg |
相比于readv /writev ,有更多的控制 flag |
recvmmsg |
sendmmsg |
在一个syscall完成多次 recvmsg /sendmmsg |
对于需要建立连接的协议(如TCP),在读写之前还需要更多的准备工作。一般的步骤如下图所示。服务端创建socket,绑定端口(bind
)然后开启监听(listen
),不断等待客户端连接(connect
)。参考链接中给出了一些特殊的情况。
这些系统调用的具体使用细节可以参考manpage。有下面一些值得一提的点:
bind()
调用的第二个参数是const struct sockaddr *addr
,但是实际上具体的地址细节由协议确定,所以对于TCP/UDP协议来说,都是实际传入sockaddr_in
,也正因为如此第三个参数要传递len
(数据是肯定要复制到内核态操作的)。accept
同理。read
&write
会返回实际读写的字节数。这在网络编程的时候尤为重要。即便我们知道客户端一次发送了10字节的数据,但是一次read
可能也会只收到4字节的数据,因为剩下的数据还没有到达。write
则是因为内核的缓冲区大小有限。
I/O Multiplexing
在上面介绍的Socket基础中,read
/write
/accept
在没有就绪的时候都会阻塞,直到操作完成或出错。但是Linux也支持非阻塞(non-blocking)模式。如果开启了非阻塞模式,无论操作完成与否都会立即返回,如果没有就绪会返回EAGAIN
提醒用户态稍后重试。
显然,光有非阻塞模式并不能提高效率——用户总不能在返回EAGAIN之后sleep一会儿,或者是直接重试吧,关键是要知道操作何时能就绪。为此,Linux提供了以下几个系统调用(集):
select
和poll
: 这两种技术允许程序监视多个文件描述符,以查看哪个或哪些文件描述符已准备好进行非阻塞I/O操作。这使得单个线程能够有效地管理多个I/O流。select
较老,智能同时监听较少的fd。epoll
: 是一种更高效的I/O事件通知方法,特别是在处理大量文件描述符时。与select和poll相比,epoll通过一种更有效的方式管理大量文件描述符的变化,减少了系统调用的开销。
[!NOTE]
select
和poll
是符合 POSIX 标准的,是标准的UNIX接口的一部分。epoll
是Linux独有的。显然,这些系统调用是阻塞的。 这些系统调用是很多语言实现异步IO的基础。简单的说,异步编程的关键是在IO发生阻塞的时候可以调度到其他任务执行,而在IO完成时继续执行该任务。异步执行框架的一个非常直观的思路就是,IO操作无法立即完成的任务都放进poll
等待,执行poll
完成的任务。