前言
我们写网络程序, 主要编写的是应用层代码.
真正要发送这个数据, 还需要上层协议调用下层协议, 也就是应用层调用传输层.
传输层给应用层提供了一组 api, 统称为 socket api, 系统给程序提供的 api 是C 风格的, JDK 针对这些 api 进行封装, 封装成 Java 风格的 api.
提供的 socket api 主要是这两组 :
- 基于 UDP 的 api
- 基于 TCP 的 api
因为 UDP 与 TCP 的协议差别很大, 所以这两组 api 差别也很大.
那这两个协议都有啥特点呢?
UDP :
无连接 (使用 UDP 的双方不需要刻意保存对端的相关信息)
不可靠传输 (消息发送完了就行, 不关注结果)
面向数据报 (以一个 UDP 数据报为基本单位)
全双工 (一条路径, 双向通信)
TCP :
有连接 (使用 TCP 的双方要刻意保存对端的相关信息)
可靠传输 (发送消息后, 知道对方是否接收到)
面向字节流 (以字节为传输的基本单位, 读写方式非常灵活)
全双工 (一条路径, 双向通信)
UDP 版的回显服务器
需要用到的 api
1.DatagramSocket API
DatagramSocket 是 UDP Socket,用于发送和接收UDP数据报。
主要用到的构造方法 :
DatagramSocket()
创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(intport)
创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)
主要用到的方法 :
void receive(DatagramPacket p)
从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacketp)
从此套接字发送数据报包(不会阻塞等待,直接发送)
void close()
关闭此数据报套接字
2.DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报.
主要用到的构造方法 :
DatagramPacket(byte[] buf, int length)
构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数 length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从offset到指定长度(第三个参数length)。address指定目的主机的IP和端口号
主要用到的方法 :
InetAddress getAddress()
从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址.
int getPort()
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获
取接收端主机端口号
byte[] getData()
获取数据报中的数据
构造UDP发送数据报时,需要传入 SocketAddress ,该对象可以使用InetSocketAddress 来创建.
服务端
public class UdpEchoServer { //首先定义一个 socket 对象, 通过 socket 对象来发送读取信息 private DatagramSocket socket = null; //绑定一个端口, 如果绑定的端口被别的进程占用了, 这里就会报错. //同一个主机上, 一个端口只能被一个进程绑定. public UdpEchoServer(int port) throws SocketException { //构造时, 指定要绑定的端口号. socket = new DatagramSocket(port); } //启动服务器的主逻辑 public void start() throws IOException { System.out.println("服务器启动!!!"); //因为服务器是要时刻读取客户端信息的, 所以使用while循环来重复读取并处理信息. while(true) { //每次循环都只做三件事 // 1.读取请求并解析 // 下面这是构造一个数据包, 就可以理解为一个餐盘, 这个餐盘里指定要放入的数据类型及大小 // 空的餐盘构建好了就得装东西了 DatagramPacket requestPacket = new DatagramPacket(new byte[666], 666); // 1kb=1024byte, UDP最多发送64kb(包含UDP首部8byte) // 通过 socket 对象来接收信息(也就是填充餐盘) socket.receive(requestPacket); //如果没接收到信息就会阻塞等待 // 为了方便我们处理这个请求, 将数据包转为 String String request = new String(requestPacket.getData(),0, requestPacket.getLength()); // 2.根据请求来计算响应(这里直接返回了) String response = process(request); // 3.把响应结果写回客户端 // 根据 response 来构造一个 DatagramPacket // 和之前构造不同, 本次构造相当于将餐盘填满, 注意还要指定发送给谁 // requestPacket.getSocketAddress()是获取客户端的IP和端口号(填充的 requestPacket 就包含了客户端的IP和地址) DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0,response.getBytes().length,requestPacket.getSocketAddress()); // 把结果发送给客户端 socket.send(responsePacket); //打印一下客户端的IP和端口号, 还有请求和响应 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 { UdpEchoServer echoServer = new UdpEchoServer(8888); echoServer.start(); } }
客户端
public class UdpEchoClient { //客户端也需要 socket 对象来进行数据交互 private DatagramSocket socket = null; private String serverIP; private int serverPort; //构造客户端时, 还需要知道服务器在哪, 才能去获取服务 public UdpEchoClient(String serverIP, int serverPort) throws SocketException { //不同与服务器, 这里没有关联端口, 不代表不需要关联端口 //而是系统会自动为客户端分配个空闲的端口 socket = new DatagramSocket(); this.serverIP = serverIP; this.serverPort = serverPort; } public void start() throws IOException { System.out.println("启动客户端!!!"); //通过 Scanner 来读取用户输入 Scanner scanner = new Scanner(System.in); while(true) { // 1.先从控制台读取一个字符串 // 打印一个提示符, 提示用户输入 System.out.print("-> "); String request = scanner.next(); // 2.把字符串构造成 UDP packet, 并进行发送 // InetAddress.getByName() 确定主机的IP地址 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(serverIP), serverPort); socket.send(requestPacket); // 3.客户端尝试读取服务器返回的响应 DatagramPacket responsePacket = new DatagramPacket(new byte[666],666); socket.receive(responsePacket); String response = new String(responsePacket.getData(),0,responsePacket.getLength()); System.out.printf("req: %s, resp: %s\n", request,response); } } public static void main(String[] args) throws IOException { UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",8888); udpEchoClient.start(); } }
启动看看效果 :
客户端 :
服务端 :
UDP 版本的字典客户端和字典服务器
要想实现单词查找功能, 首先要有一个数据结构来对单词进行存储, 一般用哈希表来存储, key 来存储单词, value 存储单词意思.
其实客户端都是一样的, 只是服务器根据请求, 返回的响应不一样了.
所以我们主要就是重写 process 方法.
服务端 :
public class UdpDictServer extends UdpEchoServer{ private HashMap<String,String> dict = new HashMap<>(); public UdpDictServer(int port) throws SocketException { super(port); dict.put("pen","笔"); dict.put("dog","狗"); dict.put("cat","猫"); //...可以无限添加, 其实网上的翻译词典什么的,就是样本比这个大 } @Override public String process(String request) { return dict.getOrDefault(request,"未找到该单词."); } public static void main(String[] args) throws IOException { UdpDictServer udpDictServer = new UdpDictServer(8888); udpDictServer.start(); } }
客户端 :
服务端 :