我们讲一下关于TCP和UDP协议的Socket编程

在UDP和TCP中,我们会区分客户端和服务器端的区别,在编写程序的时候,我们也是这么区分

Socket这个翻译过来叫做插口或者插槽,我们可以想象为一根网线,一头插在了客户端,一头插在了服务器端,然后通信,在通信前,我们需要建立一个Socket

在建立Socket的时候,我们应该如何设置参数呢?

是IPV4还是IPV6,是TCP还是UDP?

TCP基于数据流,是SOCK_STREAM UDP基于数据报,是SOCK_DGRAM

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

两端创建了Socket之后,我们的函数调用不同,可能有TCP,可能有UDP

TCP的服务端要监听一个端口,一般是先调用一个bind函数,给这个Socket赋予一个IP地址和端口,为什么需要端口呢?是因为写的是一个应用程序,当一个网络包到来的时候,内核要通过TCP头里面的端口,来找到对应的应用程序

在服务器端,有了IP号和端口,就可以利用listen函数来进行监听,在TCP的状态图里面,有一个listen状态,调用这个函数后,服务器端就进入了这个状态,这个时候客户端可以发起连接了,在内核中,每个Socket都维护了两个队列,一个是已经建立了连接的队列,三次握手都已经完毕了,处了established状态,一个是没有完全建立连接的队列,这时候三次握手都没完成,出了syn_rvcd的状态

接下来,服务器调用accept函数,拿出一个已经完成的连接进行处理,如果没有完成连接,就先等待

在服务器等待的时候,我们可以通过connect函数发起连接,在参数中指定了连接的IP地址和端口号,然后发起了三次握手,内核会给客户端分配一个临时的端口,一旦握手成功,服务器端会返回另一个端口

所以监听的Socket和真正的传输数据的Socket是两个,一个是监听的Socket,一个是已经连接的Socket

连接建立成功了,双方就可以利用read和write进行读写数据,这就好比往一个文件流中写东西一样,这个图就是TCP的Socket程序函数调用过程

图片

TCP的Socket是一个文件流,是非常对的,因为,Socket在Linux上就是以文件的形式存在的,存在着文件描述符,写入和读出,也是通过了文件描述符

Socket是一个文件,就是有文件描述符的,每一个进程都有一个数据结构task_struct 里面指向了文件描述符数组,来列出这个进程打开的所有文件的文件描述符,文件描述是一个整数,是这个数组的下标

这个数组的内容是一个指针,指向内核中所有打开的文件的列表,既然是一个文件,就会有一个inode,只不过Socket对应的inode不像是真正的文件系统一样,保存在硬盘上,而是保存在了内存中,在这个inode上,指向了socket在内核的Socket结构

在这个结构中,主要是两个队列,一个是发送队列,一个是接受队列,在这两个队列里面保存的是一个缓存的sk_buff,在这个缓存里面能够看到完整的包的结构

图片

简单来说,就是每一个进程中都有多个文件描述符,然后一个是Socket的,里面是inode,保存在了内存中,然后有一个struct_socket,就是Socket结构,其中还保留了两个队列

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

对于UDP来说,过程不一样,UDP没有连接,不需要三次握手,不需要listen和connect,但仍需要Ip和端口号,因而需要bind,UDP没有维护连接状态,也不需要每对连接建立一组Socket,而是只要有一个Socket,就能和多个客户端通信,因为没有连接状态,每次通信的时候调用senddto和recvfrom就可以通信了

图片

虽然有了这样的几个Socket函数,我们可以基本的进行网络交互了,但是这种方式,让我们只能一对一的沟通,如果是一个服务器,只能服务一个客户,肯定是不行的,服务完一个客户再去服务下一个客户端,太慢了,那么我肯定需要同时服务更多的客户

1.如何接更多的活

那么,首先需要知道我们能够到达的最大连接数,系统会通过一个四元的组来表示一个TCP连接

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

本机的IP和端口不变的情况下,只有对端的IP和端口会变化,也就是Tcp的最大连接为客户端的IP数*客户端的端口数,对于IPV4来说,就是IP的次数,2的32次方,乘以 客户端的端口号 2的 16次方,就是2的48次方,就是单机的最大TCP连接数

但是实际上,达不到这个数量,首先是文件描述符的限制,因为Socket都是文件,所以会通过ulimit在系统上限制了文件描述符的数目,其次,服务器的内存有限,无法保证那么多的连接

那么不考虑数目上限的问题,我们进行增加数目,方式主要有

方式一:创建子进程

加入我是一个代理,监听来的请求,一旦建立了一个Socket,就可以创建一个子线程,来基于这个创建的Socket的交互交给这个新的子进程来做,就好比,一个公司为了一个项目成立了一家子公司,然后将项目转移到这家子公司来做

但是有一个问题,如何创建并交给子公司的呢?

在Linux中,我们创建子进程会使用fork函数,来拷贝一个子进程,这个函数会复制文件描述符的列表,会复制内存空间,会复制一条记录执行到了哪一行程序的继承,在复制的时候调用fork,fork完成后,子进程和父进程都会记录自己刚刚执行了fork,但是fork会给父进程返回子进程的pid,而子进程则返回一个0

图片

因为复制了文件描述符的列表,这个文件描述符都是指向了整个内核统一的打开文件列表的,因而父进程因为accept创建的Socket是一个文件描述符,也会被子进程知道

而子进程就会负责这个通信,父进程可以通过刚刚获取的子进程的ID,来查看子进程的完成状态

方式二:将项目打包给独立的项目组,多线程

上面的形式发现了,如果每次接一个项目,都申请一个新进程,使用完关闭,那么消耗的资源太大了,我们不如是使用了线程,对于进程来说,线程可小的多,

在linux上,我们使用pthread_create创建了一个线程,内部也是调用了do_fork,不同的是,虽然新的线程在task里表中创建了新的一项,但是很多资源都是共享的,比如文件描述符列表,进程空间等,引用多了一个

图片

这种方式,基于了线程,但是还是有一个问题,每次新来一个TCP,都会给与其一个线程,而一个机器的线程是有上限的,那么就有一个C10K的问题,也就是1万个连接,需要一个进程或者线程,那么肯定承受不起啊

于是方法三:IO多路复用

一个线程可以看多个Socket的情况,因为都放在了一个文件描述符集合fd_set中,然后调用select函数来监听文件描述符集合是否有变化,一旦有了变化,就会依次的查看每个文件描述符,而发生变化的文件描述符的SOCKET,会在这个fd_set中将自己标记为1,表示SOCKET可以读取或者写入,这时候就进行读写,然后进行下一轮的变化

方法四:IO多路复用加回调

上面的select还是有点问题的,轮询的方式,需要我们将项目全部过一遍来查看是否有新的进度,这会导致我们查询的效率降低,因为使用select,能够同时盯住项目的数量由FD_SETSIZE限制,

加上了回调,会好的多,因为不用轮询了,而是在可以发送或者接受的时候,去主动的通知,然后项目组再根据项目的进展情况来做出相应的操作

这个函数就是epoll,在内核中的实现是通过使用了callback函数的方式,当某个文件描述符发生了变化的时候,主动通知主线程

图片

在epoll中,维护了一个数据结构,红黑二叉树,这个树上挂满了要监听的Socket对象,假如进程打开了多个Socket m x n ,然后根据epoll来监听这些Socket,就会调用epoll_create函数创建一个epoll对象,当然,epoll对象也是一个文件,对应着一个文件描述符

当使用了epoll_ctl添加一个Socket的时候,其实就是加入了一个Socket到红黑树中,同时红黑树中的节点指向了一个数据结构,

Socket中会加入这个数据结构到自己的事件队列中,当有事件来临,就从事件队列中这个数据结构获取epoll,并且调用callback通知epoll

这种方式使得监听的Socket数据增加的时候,效率不会大幅降低,同时增加了Socket的数目,这就让epoll被称为了解决 C10K问题的利器

课后总结:

1.TCP在通信的时候,客户端Scoket()后调用了connect后可以掉write和read了

服务器端socket后掉用 bind后调用listen后调用accpt最后可以write和read了

UDP则是,在socket()后bind(),然后sendto()和recvfrom()就可以了

2.在支持高并发的服务器端中,需要多线程或者多进程,为此我们延伸出了多路IO复用和epoll方法回调

课后思考:

1.epoll是linux上的函数,Window上的呢?如果想要跨平台呢?

2,自己写Socket是很复杂的,Http写起来会不会简单点呢?

1.window上是iocp,如果需要跨平台,需要兼容性更好的select,对于不同的平台,使用不同的调用

window有一个是iocp,其实proactor,而epoll是reactor,iocp效率更高,epoll通知应用去读取数据,处理完成后返回内核去发送数据,而完成端口直接在内核态进行数据处理,最后告诉应用,数据处理更加高效

epoll是异步的通知,当事件发生的时候,通过应用去调用IO函数获取数据,IOCP异步传输,当事件发生时候,IOCP机制直接将数据拷贝到缓存区中,应用直接使用

如果跨平台,可以使用libevent库,是一个事件通知库,适用于Window和Linux,BSD等多种平台

内部使用了select epoll kqueue ICOP等事件调用管理机制

2,HTTP基于了tcp协议上的,使用telnet可以轻松的创建HTTP

而对于epoll和IOCP来说,是由区别的,IOCP是封装了IO操作,可以直接进行返回,而epoll还是一个事件通知机制,需要返回到内核态,也就是并不那么快,IOCP算作是异步IO,epoll算作是同步IO

1.iocp基于了proactor,而epoll基于了reactor

发表评论

邮箱地址不会被公开。 必填项已用*标注