【EE初阶 - 网络原理】Socket 套接字
文章目录
- 1. Socket 套接字
- 1.1 概念
- 1.2 分类
- 1.3 套接字通信模型
- 1.3.1 Java数据报套接字通信模型
- 1.3.2 Java流套接字通信模型
- 1.4 Scoket编程注意事项
- 2. UDP数据报套接字编程
- 2.1 API的介绍
- 概念
- 构造方法
- 方法
- 2.1.2 DatagramPacket
- 2.1.3 InetSocketAddress ( SocketAddress的子类 )
- 2.2 实现回显服务器
- 2.2.1 实现原理
- 2.2.2 代码实现
- UDP Echo Server
- UDP Echo Client
1. Socket 套接字
1.1 概念
Socket套接字,是由系统提供用于网络通信的技术,是基于TC/IP协议的网络通信的基本操作单元.基于Socket套接字的网络程序开发就是网络编程
1.2 分类
1.3 套接字通信模型
1.3.1 Java数据报套接字通信模型
- 对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报
- Java中使用UDP协议通信,主要是基于DatagramSocket类来创建数据报套接字,并使用DatagramSocket作为发送或接收的UDP数据报
对于一次发送及接收UDP数据报的流程如下:
以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回数据,也就是只有请求,没有响应
对于一个服务端来说,重要的是提供多个客户端的请求处理并相应,流程如下:
1.3.2 Java流套接字通信模型
1.4 Scoket编程注意事项
- 1.客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同主机;
- 2.注意目的 IP 和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程;
- 3.Socket 编程我们是使用流套接字和数据报套接字,基于传输层的 TCP 或 UDP 协议,但应用层协议,也需要考虑,这块我们在后续来说明如何设计应用层协议。
- 4.关于端口被占用的问题
- 5.如果一个进程 A 已经绑定了一个端口,再启动一个进程 B 绑定该端口,就会报错,这种情况也叫端被占用。对于 java 进程来说,端口被占用的常见报错信息如下:
此时需要检查进程B绑定的是哪个端口,在查看该端口被哪个进程占用
通过端口号查进程的方式:
- 在 cmd 输入 netstat - ano | findstr 端口号,则可以显示对应进程的pid,以下 命令显示了 8888 进程的pid
- 在任务管理器中,通过PID 查找进程
解决端口被占用的问题:
- 如果占用端口的进程A不需要运行,就可以关闭A后再启动需要绑定该端口的进程B
- 如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口
2. UDP数据报套接字编程
2.1 API的介绍
概念
计算机中的文件,通常是一个广义的概念,文件IO特指的是硬盘上的文件,是狭义的文件,除此之外,文件还可以代指一些硬件设备
Socket在计算机编程汇总,也可以认为是一种特殊的文件,打开socket文件,也会在文件描述表中分配一个表项,来表示这个文件
这样的文件特指网卡这样的硬件设备
对于网卡这样的硬件设备,在操作系统终究被抽象成socket文件,这样的设定,主要是为了方便操作网卡
直接操作网卡,需要往网卡的寄存器上写一些特定的数据,不好操作;操作系统管理一些硬件设备,是抽象成文件统一管理的;把操作网卡,转化成操作Socket文件,此时 Socket 文件,就相当于网卡的 “遥控器" ;
所以 DategramSocket ,就是一个用来表示网卡的文件,通过 DategramSocket 来操作网卡,只是加了一个 Dategram 前缀,意思就是基于UDP协议进行网络通信
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
构造方法
方法
**DatagramSocket 类的 receive() & sand() 的参数类型,都是 DatagramPacket **
一个UDP数据包,就通过 DatagramPacket 对象来进行体现的,在进行receive() 或者 send(),都是按照 DatagramPacket 这样的数据包为定位进行接收的;
2.1.2 DatagramPacket
定义
DatagramPacket 是UDP Socket发送和接收的数据报
构造方法
UDP 数据包的载荷数据,可以通过构造方法来指定
方法
构造UDP发送的数据报时,需要传入SocketAddress,该对象可以使用InetSocketAddress 来创建
2.1.3 InetSocketAddress ( SocketAddress的子类 )
构造方法
2.2 实现回显服务器
2.2.1 实现原理
- 客户端给服务器发一个数据的操作,称为请求
- 服务器返回一个数据的操作,称为响应
- 一个真实的服务器,请求和响应一般是不一样的,但是为了展示上述 API 的用法,就先不去管服务器中其他复杂的逻辑,写一个最简单的回显服务器(请求是什么,响应就是什么)
2.2.2 代码实现
UDP Echo Server
(1) 构造一个 socket 对象代表网卡文件
- 输出 socket 文件内容,等于从网卡中读取数据;
- 输入 socket 文件内容,等于向网卡内发送数据
(2) 实现启动服务器的 start()
对于服务器来说,客户端啥时候发请求,发多少个请求,我们无法预测
因此服务器中通常都需要有一个死循环,持续不断的尝试读取客户端的请求数据~~
在住循环中,要实现的逻辑:
1.读取请求并解析
2.根据请求,计算相应(服务器最关键的逻辑),但我们这里写的是回显服务器,这个环节相当于省略了
3.把相应返回给客户端
每次每执行一次,就相当于处理了一次请求,处理请求的过程,典型的服务器也说上面这三个步骤
(3) 读取请求
服务器调用 receive() 对客户端发送的请求进行读取,等 receive() 方法执行完毕,参数里面对象的数据就是读取的结果:
创建一个 DatagramPacket 对象,用于接收请求
- 创建 DatagramSocket 对象的时候,需要指定一个字节数组,并且传入接收的指定长度
- DatagramPacket 表示个UDP数据报。此处传入的字节数组,就保存 UDP的载荷部分
读取网卡中的数据包
- 把 DatagramPacket 对象(对象为全0)传给 receive()
- receive() 就会在方法内部把从响应数据报中读到的数据,填充到 requestPacket 这个引用指向的对象中
- receive() 执行完毕,参数里面的对象数据,就是需要从网卡中读取的数据(请求)
这个过程就相当于我们在食堂打饭,把空的盘子(requestPacket )交给打饭阿姨( receive() ),阿姨会把打好饭(读到的数据)的盘子还给我们
(4) 解析请求
- 当前 UDP 载荷,是 DatagramSocket 对象的字节数组,存放着读取到的数据
- 这些读取到的数据是二进制数据,为了方便后续处理,我们把读取到的二进制数据转换成字符串形式
- 通过字节数组构造一个 String 对象,是构造 String 对象的一个典型做法
- 上述操作,表示拿到了一个数据报中的字节数组,把整个字节数组传给String对象,并且指定字节数组有效部分的范围,调用相应的构造方法,构成一个字符串
(5) 根据请求计算响应
- 这是服务器最关键的逻辑,但是此处写的是回显服务器,这个环节就相当于省略了
- 根据解析数据报,得到的请求 request,计算出响应 response 的操作,封装成一个方法,可以起到解耦合的作用
- 后续要写别的服务器,只需要修改 process() 的内容即可
(6) 把计算好的响应返回给客户端
- 如何根据响应 response 构造 DatagramSocket 对象呢?
- 首先,需要拿出对响应数据报进行解析操作时,创建的字符串 request 里面的字节数组
- 要传入字节数组的长度,而不是使用字符串的长度,因为字符串的单位是字符,而我们要使用字节的个数,来作为当前 responsePacket 数据包的参数
(7) 指定目的IP&目的端口
- 发送的响应数据报 responsePacket 是没有明确标注有发送的目的IP&目的端口的,要想正确地返回响应,就必须给响应数据报显式地标注目的IP & 目的端口
- 对于服务器返回响应的目的IP&目的端口,就是接到客户端请求的源IP&源端口
- 所以,通过调用 DatagramSocket 类中的 getSocketAddress(),该方法返回一个 InerSocketAddress 对象
InetSocketAddress(SocketAddress的子类)
- 这个对象包含了目标IP 和 目标端口号 (都在报头中,而不是在载荷中)
- 将这个对象作为参数,传给DatagramPacket对象,调用对应的构造方法
- 所以,将getSocketAddress()方法返回的对象,作为参数来调用对应的构造方法,实例出的responsePacket,就会显式地标注响应数据报的目标IP 和 目标端口
(8) 发送响应给客户端
- 服务器需要调send()方法,把创建好的响应数据报作为响应,返回给客户端
- send()的构造方法
- 所以,我们把刚刚构建好的响应数据报返回给客户端
(9) 打印日志来记录客户端/服务器交互的过程
(10) 判断当前 socket 对象(文件)是否需要关闭
- 文件是否需要关闭,考虑的是这个文件对象的生命周期是怎样的,此处的 socket 对象会自始至终伴随整个UDP服务器;
- 只要服务器运行,就随时可能会从客户端中读数据,如果提前关闭 socket对象,那么UDP服务器继续运行也没有意义,所以socket对象,不能在服务器运行的过程中关闭;
- 服务器关闭(进程结束),也不需要手动调用close(),因为进程结束时就会自动释放 PCB 的文件描述符表中的所有资源,
- 所以当前socket文件不手动调用 close(),也是完全没问题的,因为socket的生命周期本来就需要跟随整个进程的;
- 如果是有请求级别的文件对象,给一个请求,创建一个对象,就需要确保处理完毕之前,关闭对象。
- 所以需要结合实际情况来确认一个对象的生命周期,通过生命周期,来决定对象是否应该关闭;
(11) 补充
- DatagramPacket这个类型说是一个UDP数据报,其实也包含了一些源信息
这个类有接受IP和端口号的属性,在通过receive() 缇娜充好DatagramPacket对象后,可以直接从对象中提取出UDP数据报的来源(源IP和源端口)
所以在将DatagramPacket这个UDP数据包,作为 receive() 方法的输入型参数时,不只是把UDP中的数据读进去了,还把IP&端口号等信息也读进去了
UDP Echo Client
import java.io.IOException;
import java.net.*;
import java.util.Scanner;public class UdpEchoClient {private DatagramSocket socket = null;// UDP 本身不保存对端的信息, 就自己的代码中保存一下private String serverIp;private int serverPort;// 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.public UdpEchoClient(String serverIp, int serverPort) throws SocketException {this.serverIp = serverIp;this.serverPort = serverPort;socket = new DatagramSocket();}public void start() throws IOException {Scanner scanner = new Scanner(System.in);while (true) {// 1. 从控制台读取用户输入的内容.System.out.println("请输入要发送的内容:");if (!scanner.hasNext()) {break;}String request = scanner.next();// 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.// 构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,InetAddress.getByName(serverIp), serverPort);// 3. 发送数据报socket.send(requestPacket);// 4. 接收服务器的响应DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);// 5. 从服务器读取的数据进行解析, 打印出来.String response = new String(responsePacket.getData(), 0, responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);client.start();}
}