在了解了TCP/IP的五层模型之后,我们也就开始了网络编程的学习。在五层模型的第四层------传输层中,有两个核心协议,TCP与UDP,两者的差别非常大,编写代码的时候,也是不同的风格,因此,socket API提供了两套方法。
TCP与UDP的区别
其中,TCP的特点是有连接、可靠传输、面向字节流、全双工;UDP的特点是无连接、不可靠传输、面向数据报、全双工。
这些概念到底是什么?
有连接vs无连接
这是抽象的概念,是虚拟的或逻辑上的连接,要进行网络通信,就要进行物理上的连接。
而有连接的TCP,在TCP协议中,就保存了对端的信息,A和B通信,A和B先建立连接,让A保存B的信息,B保存A的信息。但缺点是会出现"耦合"。
而在无连接的UDP中,UDP协议本身是不保存对方的信息的。这就需要我们在自己的代码中写变量来保存对方的信息。
可靠传输vs不可靠传输
在网络上,数据是非常容易出现丢失的情况(丢包),光信号、电信号都可能受到外界的干扰。
网络世界是通过路由器/交换机来让服务器与客户端相互联系的,某个时间点,实际需要转发的数据就会超过设备能转发的上限。
于是,可靠传输,虽然不能保证数据包100%到达对方,但尽可能的提高传输成功的概率,如果出现了"丢包",也能感知到的。
不可靠传输,只要发了数据就可以,别的就不管了。
当然,并不是说可靠传输一定好,因为他的效率也会付出代价。
面向字节流vs面向数据报
面向字节流,读写数据的时候,是以字节为单位的,支持任意长度,会出现粘包问题(这个我们后面再说);面向数据报,读写数据的时候,是以一个数据报为单位的(不是字符,而且一次必须读写一个UDP数据报,不是半个),有长度限制,不存在粘包。
全双工vs半双工
全双工:一个通信链路支持双向通信(读写)
半双工:一个通信链路只支持单向通信(要么读,要么写)
socket api进行网络编程
UDP的socket api------DatagramSocket
这个就代指网卡,也就是socket文件,操作网卡的时候,流程和操作普通文件差不多,都是先打开再读写,最后关闭。操作网卡,直接操作是不好操作的,因此通过这个类把操作网卡转换成操作socket文件,socket文件就相当于"网卡的遥控器",就是电脑的网卡,把操作系统当做文件来管理,可以用来接收、发送与关闭。

构造方法:打开文件

DatagramPacket
这个表示一个完整的UDP数据报。

接下来我们用代码来实现UDP的网络编程:
这个分为服务器端与客户端两段代码,就是要创建两个类。
服务器端:

(1)创建socket对象并初始化,初始化的部分放在构造方法内部,new时将端口号port传参传入DatagramSocket方法里。这个socket对象代表网卡文件,读这个文件等于从网卡接收数据,写这个文件等于让网卡发送数据。

(2)创建方法start,创建while主循环,在主循环中,确定要做的三件事:第一,读取请求并解析;第二,根据请求,计算响应;第三,把响应返回给客户端。

(3)读取请求并解析。这里分成了三个部分:一、构造DatagramPacket对象,DatagramPacket就代表UDP的数据包(报头+载荷,new字节数组时保存)

二、用socket调用receive,把他理解为输出型参数。

三、把UDP数据包的载荷取出来,构造成一个String。
1.通过request.getData()拿到DatagramPacket中的字节数组
2.通过requestPacket.getLength()拿到有效数据长度
3.根据字节数组构造出一个String

(4)根据请求计算响应,创建process方法,返回值为请求request

(5)把响应返回给客户端,利用DatagramPacket创建新的对象,这个对象就作为发送的参数,而new时,将response.getBytes()传入是为了拿到字符串中的字节数组;response.getBytes().length是为了拿到字节数组的长度(而不是使用字符串的长度response.length);然后传入requestPacket.getSocketAddress()是为了拿到客户端的IP和端口号,以便知道要发送给谁。

当然,new DatagramPacket是为了构造响应数据报,上面的调用了getSocketAddress方法的是请求数据报。
最后,把构造好的数据包发送出去。

(6)打印一个日志(用printf而不是println),记录这次的客户端/服务器端的交互过程。

后面的主方法就实例化一个对象,把端口号传进去,再用这个对象调用start方法即可。

客户端:

这个的详细步骤都在图上,与服务器端的大同小异。注意要用代码保存一下IP与端口号:

当然,这个socket初始化时,DatagramPacket没有填写serverPort(端口号)参数,是因为客户端访问服务器,serverIP是目的IP,serverPort是目的端口,源ip客户端所在的主机ip、源端口,应该是随机搞一个端口(操作系统分配空闲端口),不同客户端不能使用同一个端口号。
而且在主方法中,参数端口号必须与服务器的主方法的端口号一致。


当我们将请求数据包作为输出型参数给receive接收后,请求数据包中就有源端口、源IP等信息,此时就能通过请求数据包得到源端口与源IP信息构造出响应数据包。
那么我有个问题:当前服务器启动了,启动之后,客户端还没有输入任何内容(客户端的请求没有到达),那么在客户端的请求到来之前,服务器里面的逻辑都在干什么?
其实这里的receive会触发阻塞行为,客户端请求发来了,receive才会返回,否则会一直阻塞。

最终实现了这样的效果:


这是在一个主机上进行的交互,理想情况是跨主机进行交互,但这样尚且不能,因为NAT在搞鬼(这个后面再讲)。
下次我们讲TCP的网络编程。