网络编程-java
Socket 套接字
Socket套接字,是由系统提供用于网络通信的技术,是基于 TCP/IP 协议的网络通信的基本单元。基于 Socket 套接字的网络程序开发就是网络编程。
应用层会调用操作系统提供的一组 api ,这组 api 就是 socket api(传输层给应用层提供)
socket => 插槽 -> 主板上的一些接口
传输层有两个核心协议:
TCP UDP 由于这两个协议差别非常大,编写代码的时候,也是不同的风格。所以,socket api 提供了两套~
TCP的特点:有链接 , 可靠传输 ,面向字节流 , 全双工
UDP的特点:无连接 ,不可靠传输 , 面向数据报 , 全双工
有链接/无连接:
有链接/无连接 是抽象的概念,虚拟的/逻辑上的链接。
要进行网络通信,物理上的链接(网线什么的)
对于 TCP 来说,TCP 协议中,就保存了对端的信息(A和B 通信,A和B先建立链接,让A保存B的信息,B保存A的信息(彼此之间知道,谁是和它建立连接的那个))
对于 UDP 来说, UDP 协议本身,不保存对方的信息 ——就是无连接 (当然可以在自己的代码中写变量,保存对方的信息,但这不是 UDP 的行为)
可靠传输 VS 不可靠传输
网络上,数据是非常容易出现丢失的情况(丢包)。像光信号/电信号,都可能受到外界的干扰。(比如本来是传输 0101,其中有些 bit 为就被修改了,这样乱了的数据就会被识别出来,把这样的数据给丢掉)
并且网络世界 是通过路由器/交换机交织起来了,路由器/交换机就类似于“十字路口”,因此就会发生“堵车”,在某个时间点,实际需要转发的数据超过了设备转发的上限。
所以,我们不能指望,一个数据包发送之后,100%到达对方。
可靠传输的意思,不是保证数据包100%到达,而是尽可能的提高传输成功的概率,如果出现丢包了,是能够感知到的。
不可传输的意思,只是把数据发了,就不管了。
可能我们只管感觉,可靠传输更好,但是可靠传输也是要付出代价的 —— 他的效率是远不如不可靠传输的。所以也是要分情况使用的。
面向字节流 VS 面向数据报
面向字节流,读写数据的时候,是以字节为单位 => 支持任意长度,但会导致粘包问题。
面向数据报,读写数据的时候,是以一个数据报为单位(不是字符) => 一次必须读写一个 UDP 数据报,不能是半个,所以不存在粘包,但长度受到限制
全双工 VS 半双工
全双工:一个通信链路,支持 双向通信 (类似能读,也能写)
半双工: 一个通信链路,只支持单向通信(要么读,要么写)
计算机中的“文件”通常是一个“广义的概念”,文件还能代指一些硬件设备(操作系统管理硬件设备,也是抽象成文件,统一管理的)。
网卡 => socket文件
操作网卡的时候,流程和操作普通文件差不多,也是 打开(也会在文件描述符表中分配一个表项) -> 读写 -> 关闭
操作网卡,直接操作不好操作,所以就把网卡转换成操作 socket 文件,socket 文件就相当于“网卡的遥控器”。
那么接下来我们就要进行 socket api 进行网络编程了,本身也是操作系统的功能。
UDP数据报套接字编程
API介绍
DatagramSocket
DatagramSocket 是 UDP Socket,用与发送和接收UDP数据报。
DatagramSocket的构造方法:(相当于打开文件)
port:端口号
创建Socket的时候,就会关联上一个 端口号,使用端口号来区分主机上不同的应用程序。
DatagramSocket的一些关键方法:
DatagramPacket
DatagramPacket 是UDP Socket 发送和接受的数据报。
DatagramPacket的构造方法:
DatagramPacket的关键方法:
构造UDP发送的数据报时,需要传入 SocketAddress,该对象可以使用 InetSocketAddress 来创建。
下来我们来创建一个回显服务器和客户端(简易):
Echo:回声
回显服务器:客户端给服务器发一个数据(请求),服务器返回一个数据(响应),请求是什么,响应就是什么。
不过真实的服务器,请求和响应是不一样的。
回显服务器,处理请求的关键步骤:
- 1.接收请求并解析
- 2.根据请求,计算响应(最关键的步骤)
- 3.发送响应给客户端
- 4.创建日志
此处有一个问题是 socket 不用close 吗?
文件要关闭,要考虑清除这个文件对象的生命周期是怎么样的,此处的 socket 对象,伴随整个 Udp 服务器,自始至终。
如果服务器关闭(进程结束),进程结束时就会自动释放PCB的文件描述表中的所有资源,也不需要手动调用 close 了。
还有一个问题:当前服务器启动之后,客户端还没有,当然也没有请求发送,那么在客户端请求过来之前, 服务器里面的逻辑都在干什么呢?
receive 会触发阻塞行为。客户端请求发来了,receive 才会返回,客户端的请求没来,receive 就一直阻塞了。
创建客户端的重要步骤:
- 1.从控制台读取用户输入的信息
- 2.将请求发送给服务器
- 3.接收服务器的响应
- 4.从服务器读取的数据进行解析,并打印
不过有一个不同的是,客户端的构造方法需要指定访问的服务器的地址和端口号
在此基础上,我们可以编写一个汉译英服务器。
我们只需要重写 process 方法。
TCP套接字编程
TCP的一个核心特点,面向字节流。读写数据的基本单位就是字节byte。
API介绍
ServerSocket:
ServerSocket是创建 TCP服务端Socket 的API。(专门给服务端用的)
构造方法:
服务器启动,需要先绑定一个端口号。
关键方法:
TCP是“有连接”,这里的 accept 联通连接的关键操作。
Socket:
Socket 是客户端Socket,或服务器中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,以及用来与对方收发数据的。
构造方法:
host 和 port 是服务器的 IP 和服务器的 端口,不是客户端自己的。
关键方法:(注意返回值)
那接下来我们就通过 TCP 来实现回显服务器和客户端的简单通信吧~~
大致思路是一样的,但还是有一些不同的。
服务器:
客户端:
这里面的 Scanner 和 PrintWriter 是针对 InputStream 和 OutputStream 套了一层。上述,我们其实已经完成输入输出⼯作,但总是有所不⽅便,所以将InputStream 和 OutputStream 处理下,使⽤Scanner 和 PrintWriter 类来完成输入输出,因为PrintWriter 类中提供了我们熟悉的 print/println/printf ⽅法。
Scanner 这里的构造方法:
PrintWriter这里的构造方法:
上述代码是我们根据 UDP 的思路来写出来的。虽然看起来没啥问题,但是运行的时候,我们会发现,虽然服务器日志这边显示出来客户端已经建立连接了,但是在客户端发送请求之后,没有响应答打印出来。这是为什么呢?
这是因为这个println方法只是把数据放到“发送缓冲区(内存空间)”中,还没有真正写入到网卡中。
解决办法就是 使用 flush 方法来“冲刷缓冲区”。这是PrintWriter 的行为,如果不套壳,是可以直接发送的,但是在实际开发中,广泛使用缓冲区这样的概念,flush 这个操作是很关键的。
加了flush就可以了。
但是还有一个问题是,一个服务器可能要同时给多个客户端提供服务,在服务器建立连接的代码上我们写了个while循环,代表的意思是可以建立多个连接。在IDEA上面默认的是一个程序只能启动一边,再次启动还是该程序。通过设置,我们可以连续创建多个客户端。
通过在不同的客户端发送请求,我们会发现只有第一个客户端的请求能得到响应,但是其他的客户端没有得到响应。
但是,把当前得到响应的客户端关闭之后,下一个客户端就会得到响应了,这是为什么呢?
在服务器中
如果只是单个线程,无法同时响应多个客户端。此处应该给每个客户端都分配一个线程,所以多线程的诞生,就是为了这个场景服务的。
除了引入多线程之外,为了避免频繁创建销毁线程,也可以引入线程池。
如果客户端进一步增加,此时多线程/线程池,就会产生出大量的线程。操作系统中内置了IO多路复用,其本质上是一个线程同时负责多个客户端的请求。
举个例子:
比如我和朋友要去小吃街吃饭,他想吃煎饼果子,我想吃肉夹馍。此时有三个方案:
方案一:
我先买煎饼果子,再去买肉夹馍。(单线程)
方案二:
我和朋友一起出发,他买煎饼果子,我买肉夹馍。(多线程)
方案三:
我先到煎饼果子这边,给老板说:“老板,来个煎饼果子,我一会过来拿”,然后又到肉夹馍那边,给老板说:“老板来个肉夹馍,我一会过来拿”,然后我就开始等,在这等的过程中,这两个小摊的老板在同时工作。等他们做好的时候,就会过来叫我。所以我只是用了一份等待的时间,同时等待两个任务的完成。
(IO多路复用)
多个客户端,对应着多个老板,每个客户端,绝大部分时间是沉默的,工作线程只需要等待,等到客户端发来数据的时候,线程再来处理就可以了,多个客户端同时发数据的概率比较小。
IO多路复用是当前开发服务器的主流的核心技术,操作系统内置的,只需要调用api即可,Java 通过 NIO 来封装了 IO 多路复用。