概念: Socket 套接字就是操作系统给应用程序提供的网络编程 API。
我们可以认为 socket api 是和传输层密切相关的。
我们知道,在传输层中,提供了两个最核心的协议,UDP TCP。
因此,socket api 中也提供了两种风格。UDP TCP。
在这里我们简单认识一下 UDP 和 TCP:
解释 有连接 / 无连接
例:
打电话就是有连接的,需要建立了才能通信。建立连接需要对方来 “接受”。
发短信,就是无连接的,直接发送即可无需接受。
解释 可靠传输 / 不可靠传输
在这里,对于可靠传输的定义是:发送方的数据到底是不是发送过去了,还是丢了。
所以,在这里:
打电话是一个可靠传输。
发短信是一个不可靠传输。
但要注意的是,可靠不可靠,与有没有连接没有任何关系。
解释 面向字节流 / 面向数据报
面向字节流: 数据传输和文件读写类似,是“流式”的。
面向数据报: 数据传输以一个个“数据报”为单位。(一个数据报可能为若干个字节,带有一定的格式)。
解释 全双工
即就是一个通信通道,可以双向传输。(既可以发送,也可以接收)
对应的 半双工 就是指只可以单向传输信息。
至于如何传递信息,与用户的 路由器,交换机配置有关。如图:
这里给出了两个类用来操作:
使用这个类表示一个 socket 对象。
在操作系统中,是将这个 socket 对象当成一个文件来处理的。
对于普通文件,对应的硬件设备是 硬盘。 对于socket 文件,对应的硬件设备是 网卡。
拥有了 socket 对象就可以与另一台主机进行通信。如果要和多个不同主机交互,就需要创建多个 socket 对象。
DatagramSocket 构造方法:
在这里就可以看出来,本质上不是 进程 和 端口 建立联系,而是进程中的 socket 对象和 端口 建立联系。
DatagramSocket 方法:
对于 void receive(DatagramPacket p) 方法:
在此处传入的相当于一个空对象,receive 方法内部,会对参数的空对象进行内容填充。从而构造出结果数据。(构造出一个数据报)
该套接字 API 表示的是 UDP 中传输的一个报文。构造这个对象可以将指定的具体数据传递进去。
DatagramPacket 构造方法:
文章中详细解释的是其中较为核心的代码,与整体逻辑还有差异,整体代码会在后面罗列。
注:这里编写的客户端服务器是一个简单 UDP 版本的服务器,称之为:回显服务器。
一个普通的服务器: 收到请求,根据请求计算响应(业务逻辑),返回响应。
回显服务器: 省略了普通服务器的 “根据请求计算响应”,这里只是为了演示 socket api 的用法。
创建服务器前的初步准备
代码如下:
//这里定义成一个私有属性方便后续直接使用
private DatagramSocket socket = null;
注:尤其对于一个服务器来讲,创建一个 socket 对象的同时,需要让其绑定一个明确的端口号。
因为在服务器在网络传输中处于一个被动的状态,没有一个明确的端口号,客户端就无法寻找到请求的服务器。
// 这里的 UdpEchoSever 是定义的服务器类名
// 这里的 port 就是一个服务器端口号
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port);
}
形象的解释上面需要端口号的原因:
例如:
假设本人现在开了一家面馆,地址在地球村,美利坚1号,这里的 “1号” 就相当于端口号。
假设本人的小面做的不错,口口相传我都在 “地球村,美利坚1号”。但是,如果我现在通过小推车贩卖小面,只是经常在 1号 门口售卖,有时到处跑,此时,客户就很难准确的找到我。
因此,固定的位置就很重要,端口也是如此!
创建服务器的核心原理以及代码
对于 UDP 来说,传输的基本单位是 DatagramPacket
这里要用 receive 方法来接受请求。
这里还需要再次说明一下关键字 receive。
这个 receive 方法是一个 输出型参数,所以,这里需要先创建出来一个 空白的 DatagramPacket 对象,交给 receive 来填充(填充的是数据来自于网卡)
//receive 方法是一个输出型参数,需要先创建好一个空白的 DatagramPacket 对象,交给 receive 方法来填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096 );
socket.receive(requestPacket);
此时,接受过来的 DatagreamPacket 是一个特殊的对象,不方便处理,这里需要将其构造成一个字符串类型。
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
解释上述字符串转换代码
如下:
我们已知,上面传递下来的元素是存储在数组中。
如上图所示,这里的数组不一定是用满的。因此,要构造字符串,构造的就是呢些使用的部分。 即就是调用 getLength()
方法获取实际长度。(0 ~ getLength() 范围的元素)
获取处理后的元素
String response = process(request);
设计根据需求计算响应方法
// 这里就是实现了一个回显
public String process(String request){
return request;
}
这里将数据返回给客户端是 服务器 的工作。
因此,这里调用的 send 方法是属于 DatagramSocket 的,但是其发送的内容单元是 DatagramPacket 类型 。
发送回客户端前也就需要将 packet 构建好。
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
// 发送组织好的信息
socket.send(responsePacket);
但是这里构造的相应对象与 接受时获取对象不同,这里构造的响应对象不可以使用空的字节数组,而是要使用响应的元素构造。
System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort()
,request,response);
整体展示 UDP 格式的服务器代码:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//Udp版本的回显服务器
public class UdpEchoSever {
//网络通信的本质是操作网卡
//但是网卡的直接操作非常不方便,在操作系统内核中,就使用了 socket 这样的文件来描述网卡
//因此要实现网络通信,就必须先创建出一个 socket 对象
private DatagramSocket socket = null;
//对于服务起来讲,创建 socket 对象同时,需要让其绑定上一个端口号
//尤其针对服务器,更需要一个准确的端口号
//因为服务器在网络传输中是处于被动的状态,没有明确地端口号,客户端就无法寻找到请求的服务器
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//要注意的是服务器不是给一个客户端服务的,需要服务很多的客户端
while(true){
//1. 读取客户端发送过来的请求
// 使用 receive 方法接受请求。
//receive 方法是一个输出型参数,需要先创建好一个空白的 DatagramPacket 对象,交给 receive 方法来填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096 );
socket.receive(requestPacket);
//此时 DatagramPacket 是一个特殊的对象,不方便处理,这里可以将数据拿出来,构造成一个字符串
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根据请求计算响应,这里是一个回显服务器,所以请求和响应相同
String response = process(request);
//3.将响应数据返回给客户端
// send 的参数也是 DatagramPacket 需要将这个 packet 构建好
// 此处构造的响应对象,不可以使用空的字节数组,而是要用响应的元素构造
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印一下,当前此次相应的中间处理结果
System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort()
,request,response);
}
}
//这个方法就是"根据需求计算响应"
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
//这里的端口可以随意设置
UdpEchoSever sever = new UdpEchoSever(9090);
sever.start();
}
}
对于客户端,我们要知道,就是来和对应的客户端进行通信的。
这里,我们就应该想起前面文章中提到的 网络通信的五元组。
如上图所示,下面我们首先来实现基本设置。
回显客户端的基本设置
private DatagramSocket socket = null;
private String severIp = null;
private int severPort = 0;
//这里是构造方法
public UdpEchoClient(String severIP,int severPort) throws SocketException {
//这里不需要设定端口,让操作系统自动分配
socket = new DatagramSocket();
this.severIp = severIP;
this.severPort = severPort;
}
首先这里构造的 socket 对象,不需要绑定一个固定的显示端口。随机挑选空闲的即可。
其次,这里的
源IP:127.0.0.1 已知。
源端口:9090 前面已经设定。
目的 IP:127.0.0.1 已知(环回IP)。
目的端口:当前已经随机分配。 已知。
最后使用的协议类型也已经明确。
到这里,基本上已经万事俱备,下面解释后面的操作。
编写客户端核心操作
这里的操作比较简单直接展示代码:
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
//1. 从控制台读取要发送的数据
//打印提示符
System.out.println("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("bye");
break;
}
这里要注意的是,此处要将信息发送出去,同样要调用 send 方法。
前面说过,send 方法中传递的元素类型是 packet 类型。所以仍然需要构造 packet 类型,并且需要将 severIP 和 port 传入。
代码如下:
// 上述 IP 地址是一个字符串,需要 InetAddress.getByName 来进行转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(severIp),severPort);
socket.send(requestPacket);
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,requestPacket.getLength());
这里和前面服务器获取元素相同,同样使用 receive 方法将返回的信息填充。
最后转换成 String 类型。
System.out.println(response);
整体展示 UDP 格式的客户端代码
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//Udp版本的回显客户端
public class UdpEchoClient {
private DatagramSocket socket = null;
private String severIp = null;
private int severPort = 0;
//一次通信需要两个 IP 两个端口
//客户端的 IP 是 127.0.0.1 已知
//客户端的 端口号 是操作系统自动分配 已知
//要进行传输,服务器的 IP 和 端口号 也需要传递给 客户端
public UdpEchoClient(String severIP,int severPort) throws SocketException {
//这里不需要设定端口,让操作系统自动分配
socket = new DatagramSocket();
this.severIp = severIP;
this.severPort = severPort;
}
public void start() throws IOException {
System.out.println("客户端启动");
Scanner scanner = new Scanner(System.in);
//这里不只是一个客户端访问服务器
while(true){
//1. 从控制台读取要发送的数据
//打印提示符
System.out.println("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("bye");
break;
}
//2. 构造成 UDP 请求,并发送
// 构造这个 Packet 的时候,需要将 severIP 和 port 传入。但是此处的 IP 需要的是一个 32 位的整数形式
// 上述 IP 地址是一个字符串,需要 InetAddress.getByName 来进行转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(severIp),severPort);
socket.send(requestPacket);
//3. 读取服务器的 UDP 响应,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
// 将返回回来的信息构造为 String 字符串
String response = new String(responsePacket.getData(),0,requestPacket.getLength());
//4. 将解析好的结果显示出来
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
有关 UDP 的使用以及相关工作逻辑到此已经基本解释完毕,文笔浅薄,如有不足之处欢迎指出。
码子不易,您小小的点赞是对我最大的鼓励!!!