浅谈Socket编程

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

之前我们讲了TCP和UDP协议,现在我们来讲一下基于TCP和UDP协议的Socket编程。

Socket起源于Unix,在Unix一切皆文件的思想下,Socket是一种’‘打开一读/写一关闭''模式的实现,服务器和客户端各自维护一个“文件”,在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束是关闭文件。

Socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。

浅谈Socket编程

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket。Socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。如果说HTTP是轿车,提供了封装或者显示数据的具体形式;Socket就是发动机,提供了网络通信的能力。

Socket编程进行的是端到端的通信,往往意识不到中间经过多少局域网,多少路由器,因而能够设置的参数,也只能是端到端协议之上网络层和传输层的。在网络层,Socket函数需要指定到底是IPv4还是IPv6,分别对应设置AF_INET和AF_INET6,还要指定到底是TCP还是UDP。TCP协议是基于数据流的,所以设置SOCK_STREAM,而UDP是基于数据报的,因而设置为SOCK_DGRAM。

基于TCP协议的Socket程序函数调用过程

浅谈Socket编程

TCP的服务端要先监听一个端口,一般是先调用bind函数,给这个Socket赋予一个IP地址和端口。内核通过是端口找到应用程序;IP地址(一个机器可能多张网卡,就会有多个IP地址,可以选择监听所有的网卡或者一个网卡,只有发这个网卡的包,才会给你),有了IP和端口号,就可以调用listen函数进行监听。然后客户端发起连接。

在内核中,为每个Socket维护两个队列。一个是已经建立连接的队列,三次握手已经完毕,处于establish状态;一个是还没有完全建立连接的队列,这个时候三次握手还没有完成,处于syn_rcvd状态。

然后服务端调用accept函数,拿出一个已经完成的连接进行处理。如果还没有完成就要等着。在服务端等待的时候,客户端通过connet函数发起连接(参数中指明要连接的IP地址和端口号),然后发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept就会返回另一个Socket。之后就通过read和write函数来读写数据,就像往一个文件流中写东西一样。

监听的Socket和用来真正传数据的Socket是两个,一个叫做监听Socket,一个叫做已连接Socket

基于UDP协议的Socket的函数调用过程

UDP协议是没有连接的,但是交互仍然需要IP和端口号,因而也需要bind。因为没有连接状态,所以不需要每对连接建立一组Socket,而是只要有一个Socket,就能够和多个客户端通信。也正式因为没有连接状态,每次调用的时候,都调用sendto和recvfrom,都可以传入IP地址和端口。

浅谈Socket编程

服务器如何接更多的项目

先来算一下理论上的最大的连接数。系统会用一个四元组来表示一个TCP连接。

{本机 IP, 本机端口, 对端 IP, 对端端口}

服务器通常固定在某个本地端口监听,等待客户端的请求。所以就客户端的端口IP和端口数的可变的(对端IP和对端端口)。最大TCP连接数 = 客户端IP数 * 客户端端口数。对IPv4,客户端的IP数最多为2的32次方,客户端端口数最多为2的16次方,也就是服务端单机最大TCP连接数,约为2的48次方。但是由于操作系统内存有限和文件描述符限制最大连接数远远不能达到理论上限。所以如果资源有限的情况下想接更多的项目,就需要降低每个项目的消耗资源数。

  • 多进程方式(fork)

  • 多线程方式(pthread_create,也是调用do_fork)

  • 一个项目组支撑多个项目(IO多路复用,一个线程维护多个Socket),这个时候每个项目组都应该有个项目进度墙,将自己组看的项目列在那里,然后每天通过项目墙看每个项目的进度,一旦某个项目有了进度,就派人去盯一下。

    由于Socket是文件描述符,因而某个线程盯的所有的Socket,都放在一个文件描述符集合fd_set中,这就是项目进度墙,然后调用select函数来监听文件的描述符是否有变化。一旦有变化就会依次查看每个文件描述符。那些发生变化的文件描述符在fd_set对应的位都设置为1,表示Socket可读或者可写,从而可以进行读写操作,然后继续调用select,继续盯着下一轮变化。这样其实有个问题就是每次变化的时候都要通过轮询的方式来查看进度,很影响一个项目组能够支撑的最大项目数量。所以使用select能够同时盯的项目数量由FD_SETSIZE限制。

  • 一个项目组支撑多个项目(IO多路复用,从“派人盯着”到“有事通知”),通过事件通知的方式,项目组不需要通过轮询挨个盯着这些项目,而是当项目进度发生变化的时候,主动通知项目组,然后项目组再根据项目进展情况做相应的操作。

    完成这个事情的函数叫epoll,它在内核中的实现不是通过轮询的方式,而是通过注册callback函数的方式,当某个文件描述符发生变化的时候,就会主动通知。

浅谈Socket编程

如图所示,假设进程打开了Socket m,n,x等多个文件描述符,现在需要通过epoll来监听是否这些Socket都有事件发生。其中epoll_create创建一个epoll对象,也是一个文件,也对应一个文件描述符,同样也对应着打开文件列表中的一项。在这项里面有一个红黑树,在红黑树里,要保存这个epoll要监听的所有Socket。

当epoll_ctl添加一个Socket的时候,其实是加入这个红黑树,同时红黑树里面的节点指向一个结构,将这个结构挂在被监听的Socket的事件列表中。当一个Socket来了一个事件的时候,可以从这个列表中得到epoll对象,并调用callback通知它。

这种通知方式使得监听的Socket数据增加的时候,效率不会大幅度降低,能够同时监听的Socket的数目也非常的多了。上限就为系统定义的,进程打开的最大文件描述符个数。因而,epoll被称为解决C10K问题的利器

写在最后

http和用Socket写的通信程序的区别:

我们来看看网络的五层架构,应用层在最上面,然后TCP/UDP在传输层,IP在网络层,http,ftp是基于tcp开发的,所以他们是应用层,基于tcp/ip这些协议进行二次开发所得的产物就属于应用层。所以我们用socket写的网络通信程序就是应用层,只不过我们开发的东西并没有抽象成一个新协议,这就是开发的东西和http的区别。

浅谈Socket编程

原文始发于微信公众号(九局下半大逆转):浅谈Socket编程