浅谈网络编程

2021-09-26 10:16:05 浏览数 (1)

OSI协议,可分七层、五层、四层, 七层划分为:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层五层划分为:应用层、传输层、网络层、数据链路层、物理层四层划分为:应用层、传输层、网络层、网络接口层。 OSI七层模型: 物理层是OSI的第一层,它虽然处于最底层,却是整个开放系统的基础。物理层为设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。 【物理层要形成适合数据传输需要的实体,为数据传送服务。一是要保证数据能在其上正确通过,二是要提供足够的带宽(带宽是指每秒钟内能通过的比特(BIT)数),以减少信道上的拥塞。传输数据的方式能满足点到点,一点到多点,串行或并行,半双工或全双工,同步或异步传输的需要。】 数据链路层可以粗略地理解为数据通道。例如:以太网协议Ethernet,对应独立的链路产品中最常见的当属网卡,网桥也是链路产品,即MAC地址。 网络层在具有开放特性的网络中的数据终端设备,都要配置网络层的功能.现在市场上销售的网络硬设备主要有网关和路由器,即IP地址 传输层是两台计算机经过网络进行数据通信时,第一个端到端的层次,具有缓冲作用。电话交换网,分组交换网,公用数据交换网,局域网 即TCP协议,UDP协议 会话层可使应用建立和维持会话,并能使会话获得同步。会话层,表示层,应用层构成开放系统的高3层 建立会话顺序:1将会话地址映射为运输地址 2选择需要的运输服务质量参数(QOS) 3对会话参数进行协商 4识别各个会话连接 5传送有限的透明用户数据 表示层表示层的作用之一是为异种机通信提供一种公共语言,以便能进行互操作。【数据的压缩、解压、加密、解密都在该层完成,对图片和文件格式信息进行解码和编码。】 应用层向应用程序提供服务,这些服务按其向应用程序提供的特性分成组,并称为服务元素。 【TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。 参考文章:https://blog.csdn.net/taotongning/article/details/81352985 其中应用层包括: 1、超文本传输协议(HTTP):万维网的基本协议; 2、文件传输(TFTP简单文件传输协议); 3、远程登录(Telnet),提供远程访问其它主机功能, 它允许用户登录internet主机,并在这台主机上执行命令; 4、网络管理(SNMP简单网络管理协议),该协议提供了监控网络设备的方法, 以及配置管理,统计信息收集,性能管理及安全管理等; 5、域名系统(DNS),该系统用于在internet中将域名及其公共广播的网络节点转换成IP地址。 其次网络层包括: 1、Internet协议(IP); 2、Internet控制信息协议(ICMP); ICMP消息在以下几种情况下发送:当数据报不能到达目的地时,当网关的已经失去缓存功能,当网关能够引导主机在更短路由上发送。 IP并非设计为设计为绝对可靠,这个协议的目的是为了当网络出现问题的时候返回控制信息,而不是使IP协议变得绝对可靠,并不保证数据报或 控制信息能够返回。一些数据报仍将在没有任何报告的情况下丢失。上层协议必须使用自己的差错控制程序来判断通信是否正确。 ICMP信息通常报告在处理数据报过程中的错误。若要避免信息无限制地返回,对于ICMP消息不会单独成包发送,而且ICMP信息只在处理数据报偏移量为0时发送。 3、地址解析协议(ARP); ARP协议的工作过程描述如下: 1、PC1希望将数据发往PC2,但它不知道PC2的MAC地址,因此发送了一个ARP请求,该请求是一个广播包,向网络上的其它PC发出这样的询问:“192.168.0.2的MAC地址是什么?”,网络上的其它PC都收到了这个广播包。 2、PC2看了这个广播包,发现其中的IP地址是我的,于是向PC1回复了一个数据包,告诉PC1,我的MAC地址是00-aa-00-62-c6-09。PC3和PC4 收到广播包后,发现其中的IP地址不是我的,因此保持沉默,不答复数据包。 3、PC1知道了PC2的MAC地址,它可以向PC2发送数据了。同时它更新了自己的ARP缓存表,下次再向PC2发送信息时,直接从ARP缓存里查找PC2的MAC地址就可以了,不需要再次发送ARP请求。 4、反向地址解析协议(RARP)。 RARP的工作过程如下: 1、网络上的每台设备都会有一个独一无二的硬件地址,通常是由设备厂商分配的MAC地址。PC1从网卡上读取MAC地址,然后在网络上发送一个RARP请求的广播数据包,请求RARP服务器回复该PC的IP地址。 2、RARP服务器收到了RARP请求数据包,为其分配IP地址,并将RARP回应发送给PC1。 3、PC1收到RARP回应后,就使用得到的IP地址进行通讯。 我们先介绍一下在5层网络模型中应用通过TCP发送数据的流程: 对于应用层来说,只关心发送的数据DATA,将数据写入socket在内核中的缓冲区SO_SNDBUF即返回,操作系统会将SO_SNDBUF中的数据取出来进行发送。 传输层会在DATA前面加上TCP Header,构成一个完整的TCP报文。 当数据到达网络层(network layer)时,网络层会在TCP报文的基础上再添加一个IP Header,也就是将自己的网络地址加入到报文中。 到数据链路层时,还会加上Datalink Header和CRC【循环冗余校验(Cyclic Redundancy Check, CRC)是一种根据网络数据包或计算机文件等数据产生简短固定位数校验码的一种信道编码技术】。 当到达物理层时,会将SMAC(Source Machine,数据发送方的MAC地址),DMAC(Destination Machine,数据接受方的MAC地址 )和Type域加入。 浅解TCP粘包、拆包 我们都知道TCP属于传输层的协议,传输层除了有TCP协议外还有UDP协议。那么UDP是否会发生粘包或拆包的现象呢?答案是不会。UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。而TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。 粘包、拆包表现形式 现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下: 第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。

粘包、拆包发生原因 发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充, 1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。 2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。 3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。 4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。 等等。 粘包、拆包解决办法 通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个: 1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。 2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。 3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。 等等。

什么是网络编程?例如: 我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:

  • 用 read() 读取从远程计算机传来的数据;
  • 用 write() 向远程计算机写入数据。

你看,只要用 socket() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!

一般来说,PHP要进行多进程编程,比较常见的是:

1. 要进行大量的网络耗时的操作

2. 要做大量的运算,并且,系统有多个cpu,为了让用户有更快的体验,把一个任务,分成几个小任务,最后合并。

所以,应该尽量不要在调用的地方有太多复杂的逻辑,把逻辑内置在服务中。

讲到进程编程,我们先了解常见的通信方式

  1. 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。(单工通信:计算机和打印机;半双工:对讲机;全双工:电话【两个信道】)
  2. 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点
  4. 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
  5. 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  6. 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
  7. 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

管道通信例子

使用函数penctl_fork,环境:unix

pcntl_fork — 在当前进程当前位置产生分支(子进程)。译注:fork是创建了一个子进程,父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程 号,而子进程得到的是0。

通过管道通信的大概思路是,首先创建一个管道,然后子进程向管道中写入信息,父进程从管道中读取信息,这样就可以做到父子进程直接实现通信了:

代码语言:javascript复制
<?php
/**
 * author: Mark
 */
//创建管道
$pipePath = "/tmp/test.pipe";//指定一个管道的路径,这里跟普通文件没什么区别。
if( !file_exists( $pipePath ) ){
    if( !posix_mkfifo( $pipePath, 0666 ) ){//通过 posix_mkfifo 函数创建 管道 并且设置读写权限为 0666
        exit('make pipe false!' . PHP_EOL);
    }
}

//创建进程,子进程写管道,父进程读管道
$pid = pcntl_fork();

if( $pid == 0 ){//两个进程根据当前进程所获得的$pid的值不同,而进入不同的分支。
    //子进程写管道
    $file = fopen( $pipePath, 'w' );
    fwrite( $file, 'hello world' );//子进程打开管道,并向其中写入hello world ,然后进入休眠,休眠结束之后,退出。
    sleep(1);
    exit();
}else{
    //父进程读管道
    $file = fopen( $pipePath, 'r' );//父进程打开管道,并进行读取,最后执行 29行的代码回收掉子进程。这里面两个地方是阻塞的,首先是默认读的地方,要等待子进程发出exit命令之后,才能返回数据。还有就是回收进程的 pcntl_wait方法。要等到进程退出。
    //stream_set_blocking( $file, False );  //设置成读取非阻塞
    echo fread( $file, 20 ) . PHP_EOL;

    pcntl_wait($status);  //回收子进程
}

在linux 下运行该代码:

  会看到程序阻塞 1秒 之后,输出 hello world。

  当我们打开 stream_set_blocking( file, False ); 并将下边那行改为 var_dump(fread( file, 20 )) . PHP_EOL; 时,运行程序:

  能看到程序立马输出 空串,并等待 1秒 中之后退出。这是因为。当读取是非阻塞的情况下,父进程进行读取信息的时候,不会等待立马有信息,管道中没有信息,也会立马返回。然后执行到 29行回收子进程的时候,阻塞等待子进程退出后结束。

管道和消息队列的区别

消息队列:用于消息,不是简单的对数据信息传递,消息队列还包括消息有优先级、消息到达通知等丰富内容。 管道:低级的通信机制,消息队列比管道高级多了,管道分PIPE和FIFO,PIPE是无名的,所以只能在进程内或父子进程间通信,FIFO可任何两个进程间通信了。不过这两个依然比较低级

管道通信方式的中间介质是文件,通常称这种文件为管道文件。两个进程利用bai管道文件进行通信时,一个进程为写进程,另一个进程为读进程。写进程通过写端(发送端)往管道文件中写入信息;读进程通过读端(接收端)从管道文件中读取信息。两个进程协调不断地进行写、读,便会构成双方通过管道传递信息的流水线。

利用系统调用PIPE()创建一个无名管道文件,通常称为无名管道或PIPE;利用系统调用MKNOD()创建一个有名管道文件,通常称为有名管道或FIFO。 PIPE是一种非永久性的管道通信机构,当它访问的进程全部终止时,它也将随之被撤消;它也不能用于不同族系的进程之间的通信。而FIFO是一种永久的管道通信机构,它可以弥补PIPE的不足。 管道文件被创建后,便可对它进行读写操作,通过系统调用WRITE()和READ()来实现。通信完毕后,可将管道文件关闭,用CLOSE()来实现。

同步异步阻塞非阻塞

1.同步与异步 同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication) 所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。 换句话说,就是由*调用者*主动等待这个*调用*的结果。

而异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。

典型的异步编程模型比如Node.js

举个通俗的例子: 你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。 而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

2. 阻塞与非阻塞 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

还是上面的例子, 你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。 在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

Socket编程

tcpsocket 实现 实现模型: 1.服务器端 socket -> bind -> listen -> accept(阻塞,三次握手)-> send。 2.客户端 socket -> connect(阻塞,三次握手)-> rcv。

名词解释:sockfd : socket函数成功时候返回的套接字描述符。

下面介绍三种常用的高性能套接字编程方法。

I/O多路复用之select函数

select系统调用是用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这⾥里等待,直到被监视的文件句柄有一个或多个发⽣生了状态改变。关于文件句柄,其实就是⼀一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。

select的优缺点 优点: (1)select的可移植性好,在某些unix下不支持poll. (2)select对超时值提供了很好的精度,精确到微秒,而poll式毫秒。 缺点: (1)单个进程可监视的fd数量被限制,默认是1024。 (2)需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。 (3)对fd进行扫描时是线性扫描,fd剧增后,IO效率降低,每次调用都对fd进行线性扫描遍历,随着fd的增加会造成遍历速度慢的问题。 (4)select函数超时参数在返回时也是未定义的,考虑到可移植性,每次超时之后进入下一个select之前都要重新设置超时参数。

I/O多路复用之poll函数

poll函数实现原理 (1)将需要关心的文件描述符放进fds【fds:结构体指针】数组中 (2)调用poll函数 (3)函数成功返回后根据返回值遍历fds数组,将关心的事件与结构体中的revents相与判断事件是否就绪。 (4)事件就绪执行相关操作。

poll函数的优缺点 优点: (1)不要求计算最大文件描述符 1的大小。 (2)应付大数量的文件描述符时比select要快。 (3)没有最大连接数的限制是基于链表存储的。 缺点: (1)大量的fd数组被整体复制于内核态和用户态之间,而不管这样的复制是不是有意义。 (2)同select相同的是调用结束后需要轮询来获取就绪描述符。

I/O多路复用之epoll函数

epoll函数是多路复用IO接口select和poll函数的增强版本。显著减少程序在大量并发连接中只有少量活跃的情况下CPU利用率,他不会复用文件描述符集合来传递结果,而迫使开发者每次等待事件之前都必须重新设置要等待的文件描述符集合,另外就是获取事件时无需遍历整个文件描述符集合,只需要遍历被内核异步唤醒加入ready队列的描述符集合就行了 。

epoll函数的优缺点 优点: epoll的优点: (1)支持一个进程打开大数目的socket描述符(FD) select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。 (2)IO效率不随FD数目增加而线性下降 传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行 操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。 (3)使用mmap加速内核与用户空间的消息传递。 这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。 (4)内核微调 这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。 比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小 — 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网 卡驱动架构。

0 人点赞