跳转至

Socket

Socket基础

在Unix/Linux系统中,Socket 也被视为一种文件。所以基本的编程方法也是:打开-读写-关闭。

使用socket()函数创建一个新的 Socket,类似于打开文件,返回一个整数类型的文件描述符。例如:

// int socket(int domain, int type, int protocol);
int socket_fd_ = socket(AF_INET, SOCK_DGRAM, 0);

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)。参考链接中给出了一些特殊的情况。

TCP

这些系统调用的具体使用细节可以参考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提供了以下几个系统调用(集):

  • selectpoll: 这两种技术允许程序监视多个文件描述符,以查看哪个或哪些文件描述符已准备好进行非阻塞I/O操作。这使得单个线程能够有效地管理多个I/O流。select较老,智能同时监听较少的fd。
  • epoll: 是一种更高效的I/O事件通知方法,特别是在处理大量文件描述符时。与select和poll相比,epoll通过一种更有效的方式管理大量文件描述符的变化,减少了系统调用的开销。

[!NOTE] selectpoll 是符合 POSIX 标准的,是标准的UNIX接口的一部分。epoll是Linux独有的。显然,这些系统调用是阻塞的。 这些系统调用是很多语言实现异步IO的基础。简单的说,异步编程的关键是在IO发生阻塞的时候可以调度到其他任务执行,而在IO完成时继续执行该任务。异步执行框架的一个非常直观的思路就是,IO操作无法立即完成的任务都放进poll等待,执行poll完成的任务。

参考链接