套接字编程
套接字编程也叫Socket
编程。这个章节将总结和归纳Linux操作系统下如何利用系统接口进行网络编程。
网络字节序
之前有讲过字节序这个概念,不同的主机往往有着不同的数据存储协议,分为大端以及小端两种,但既然是网络通信,不同主机之间就必须要统一规定一个字节序来规定数据传输方式,这个就被称为网络字节序。好在系统中有一些系统接口ntohs
和htons
来帮助我们完成网络字节序和本机字节序之间的相互转换。
传输层协议
网络通信是两端通信,客户端与服务端。主动发起请求的是客户端,被动接受请求的一段是服务端。永远是客户端先向服务端发送数据。通信中数据需要经过层层封装,每一层都有典型协议,但是传输层有两个协议,TCP/UDP协议。
协议特点
TCP协议特点:传输控制协议,面向连接,可靠传输,提供字节流传输服务。
UFP协议特点:用户数据报协议,无连接,不可靠,面向数据报。
TCP为了保证可靠传输牺牲了性能,因此适用于文件/压缩包/程序的传输;UDP速度快但是不够安全可靠,因此多应用于视频在线观看的传输。
UDP网络通信编程
流程
1、创建套接字,是进程与网卡直接建立关联。在内核中会创建一个socket
结构体。在这个结构体中会包含很多与网络通信有关的信息。
2、为套接字绑定地址信息(ip/port)。为了告诉操作系统哪些数据应该由这个进程处理。在操作系统内核中每一个套接字都会有一块缓冲区,上面存放着这个套接字绑定的地址信息所属的进程应该接收的数据。网络通信过程中操作系统会把本机上所有进程需要接收数据统一放进套接字缓冲区后再发送给绑定的进程,同样的发送数据也是一样的原理,因此才需要跟操作系统内核中的套接字绑定地址信息来认领属于自己的那一块缓冲区。
3、客户端首先向服务端发送数据。服务端指定对端的地址,这时候socket
就会将数据从绑定的地址发送出去。通常服务端必须固定一个地址信息,不能随意改变,保证客户端能够连上固定的服务器。但是客户端的地址可以随意,因为数据先由客户端发送,发送给服务端,服务端就能获知客户端的地址。
4、服务端接收数据。客户端发送的数据道道服务端主机后,服务端操作系统根据这个数据的地址信息决定将这个数据放到哪一个套接字的缓冲区中。服务端通过创建套接字返回的描述符,在内核中找到套接字结构体,进而从缓冲区中取出数据。
5、关闭套接字,释放内核中套接字占用的资源。
接口
1 | //创建套接字 |
实现
这里首先完全使用C来完成服务端的功能,虽然没有经过封装流程过于复杂但是可以帮助我们更好的理解udp通讯的流程。
1 | /** |
接下来使用Cpp对udp通讯再进行一次封装,这样可以方便我们之后的使用,使用也会更有模块化,之后用其实现客户端,这里可以选择把客户端地址信息写死,或者不绑定系统自己分配都可以。
1 | /** |
然后我们把他们都跑起来完成通讯。
1 | (客户端发送数据) |
TCP网络通信编程
流程
TCP建立连接比UDP更为复杂一些,因为为了保证安全必须点对点一对一进行建立连接,于是TCP建立连接就产生了所谓的三次握手建立连接。
关于三次握手建立连接的解读这里给上一篇文章,这篇文章解读清晰易懂,有理有据,提供参考。
https://baijiahao.baidu.com/s?id=1614404084382122793&wfr=spider&for=pc
大概来说只有三次握手才能避免丢包延迟等情况造成的连接无效,才能完全确认连接已经建立。为了安全建立连接这是udp所没有的。
站在应用层面,我们服务端为了接收客户端的连接请求,需要有以下步骤:
1、建立套接字。
2、绑定地址信息。
3、服务端开始监听。
4、关闭每一个连接套接字,以及皮条套接字。
但是这里要注意tcp的套接字与udp不同一个套接字只能与一台客户端建立连接,而不是一个套接字即可接收所有发往本机的所有数据。一但一个套接字与一个主机已经建立连接,它的状态就会改变为已建立连接状态将无法再监听其他客户端的连接请求。那么此时其他客户端想要通讯我们的套接字还在与上一个主机通信呢其他客户端就都会无法连接到服务端,这该如何处理呢?
这里tcp在处理时利用了一种特殊的机制,这种机制十分类似于拉皮条。没错就是拉皮条,我们最开始建立的套接字只是一个监听套接字,这个套接字就是拉皮条的,我们称之为皮条套接字,开始监听后,只要有客户端想要与这个服务端建立连接,我们的皮条套接字就会自己创建一个新的套接字与客户端进行绑定,并将新产生的套接字返回给我们,我们就可以利用这个新的套接字与指定的客户端进行通信。意思是说我们的皮条套接字并不实质与服务端通信,它只负责创建新的套接字为客户端提供一对一服务。
当我们的服务端开始监听后,我们皮条套接字就开始工作了,表示客户端此时可以进来进行通信了,并且还是一对一服务哦,但是建立连接也是需要时间的,三次握手嘛,并且一对一通信也需要时间,如果此时客户端不断向服务端发送连接请求,每一个请求都会创建新的套接字,这会消耗大量资源,在高峰期可能资源就会耗尽。为了防止这样的情况发生,tcp在监听时会创建一个队列,我们称之为未完成连接队列,我们的皮条套接字会为这个队列依次创建套接字进行连接,如果这个队列满了,客户端此时就不能再连接服务端了,皮条套接字也不用再创建新的套接字了,着手完成眼下的套接字连接以及通信,以此来控制资源。至于这个队列有多大,我们可以在监听时来指定这个队列的大小。
4、获取新创建的套接字描述符进行通信。在创建连接后,我们得从皮条套接字那里获取新的建立连接的套接字才能进行通信。
对于客户端来说,为了和服务端建立连接,也要有以下这些步骤:
1、创建套接字。
2、绑定地址信息。
3、向服务端发起三次握手建立连接,这里需要给入服务端的地址信息。
4、接收发送数据。这里的接收和发送数据不需要再想udp呢样每次都必须给如详细的对端地址信息了,这里已经有套接字建立了稳定连接,只需要传入指定的套接字描述符即可。
5、关闭套接字。
从流程可以看出,udp和tcp在建立套接字和绑定地址信息上没有太大区别,区别主要在tcp在进行数据传输前要先建立一次连接,连接建立完成后使用套接字描述符即可进行数据传输。
接口
创建套接字与绑定地址信息的接口以及关闭套接字都与udp一致。
1 | //服务端监听 |
实现
1 | /** |
这里是对tcp的接口进行一次封装,接下来实现客户端与服务端。
客户端:
1 | #include "tcp_socket.hpp" |
服务端:
1 | /** |
使用:
1 | [misaki@localhost Net]$ ./tcpserver 192.168.11.128 9000 |
但是在这里发现进行完一轮通信后,服务端无法再接收到客户端新的数据,这是因为此时监听套接字(我们还是叫的好听点)和通信套接字是在同一个进程中共同工作,此时监听套接字阻塞在了监听新的客户端,已经建立好的套接字就无法继续通信。为了解决这个问题我们必须使监听套接字和通信套接字共同同时工作,因此就牵扯到了并行的问题,这里有两种解决方案,利用多进程,或者多线程。
客户端代码是不用改变的,此时要改的只有服务端的代码。
1、多进程解决。
1 | /** |
使用:
1 | [misaki@localhost Net]$ ./tcpserver 192.168.11.128 9000 |
这样就能进行多轮通信。
2、多线程解决。
1 | /** |
使用:
1 | [misaki@localhost Net]$ ./tcpserver 192.168.11.128 9000 |
这里起了两个客户端都是可以直接进行通信的,但是要注意一点有时候线程分离线程没有及时关闭情况下我们的数据有可能还会给已经关闭了的服务端发过去。