目录
回显服务器 (Echo Server) 就是客户端发送什么样的请求, 服务器就返回什么样的响应, 没有任何的计算和处理逻辑.
一. UDP 回显服务器
1. UDP Echo Server
下面实现服务器.
public class UdpEchoSever {
private DatagramSocket socket = null;
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port); // 创建一个DatagramSocket对象,并绑定一个端口号.
}
}
(1) 这里声明的SocketException是IOException的子类, 是网络编程中常见的异常.
(2) UdpEchoSever的构造方法方法中, 在调用DatagramSocket的构造方法时, jvm就会调用系统API, 完成 "端口号 - 进程" 的绑定.
(3) 同一时刻, 一个端口号只能绑定一个进程; 而一个进程可以绑定多个端口号.
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 此处通过一个"死循环"来不停地处理客户端的请求.
// 1. 读取客户端的请求并解析.
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求, 计算响应. (回显服务器, 响应==请求)
String response = process(request);
// 3. 把响应写回到客户端.
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(), requestPacket.getPort(),
request, response);
}
}
接下来我们通过start()方法编写服务器的核心流程
1. 读取客户端请求并解析
(1) 服务器的主要工作, 就是不停地处理客户端发来的请求. 所以需要写一个 while(true) 死循环来不停地处理客户端发来的请求.
(2) 这里的 receive 方法: 一调用receive方法, 就会就从网卡上读取数据, 但是此时网卡上不一定有数据, 如果网卡上有数据, receive立即返回获取到的数据; 如果网卡上没数据, receive就会阻塞等待, 一直等待到获取到数据为止. 此处receive中的的参数也是"输出型参数", 从网卡中获取到的数据会存到requestPacket里面.
(3) receive接收到的数据是二进制数据, 为了方便后续处理, 我们把它转成字符串类型的数据.
2. 根据请求, 计算响应
因为我们这里实现的是回显服务器, 所以响应 == 请求.
3. 把相应写回客户端
由于我们为了方便处理吧字节数组转成了字符串, 所以在往回发的时候需要再基于字符串构造出字节数组. 并且, 由于UDP是无连接的, 所以通信双方不包含对端信息, 所以在往回发的时候, 我们还要带上客户端信息. 客户端信息可以从请求中拿到. getSocketAddress()这个方法就会返回客户端的IP和端口号.
4. 打印日志
那么这样的话服务器端的代码就完成了.
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoSever {
private DatagramSocket socket = null;
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port);
}
// 通过start()方法启动服务器核心流程
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 此处通过一个"死循环"来不停地处理客户端的请求.
// 1. 读取客户端的请求并解析.
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求, 计算响应. (回显服务器, 响应==请求)
String response = process(request);
// 3. 把响应写回到客户端.
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(), requestPacket.getPort(),
request, response);
}
}
private String process(String request) {
return request;
}
}
此处还有一个小问题, 这里我们创建了Socket对象, 使用完成之后应该关闭资源啊, 但是我们的代码里并没有写close() --> 主要是因为这里Socket的生命周期是跟随进程的, 进程退出, Socket资源自然也就关闭了.
2. UDP Echo Client
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String severIP;
private int severPort;
public UdpEchoClient(String severIP, int severPort) throws SocketException {
socket = new DatagramSocket(); // 服务器创建Socket对象, 一定要指定端口号 (服务器必须是指定了端口号, 客户端发起的时候, 才能找到服务器),
// 但是客户端这里最好不要指定端口号 因为我们不知道客户端那个端口繁忙, 那个端口空闲, 所以我们手动指定, 让系统去指定一个最合适的端口.
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();
// 2. 构造出一个UDP请求数据报, 发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(this.severIP), this.severPort);
socket.send(requestPacket);
// 3. 从服务器读取到响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
// 4. 把响应打印到控制台上.
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", 9071);
// 这里的127.0.0.1是特殊的IP地址, 是"环回IP" 这个IP代表"本机", 如果客户端和服务器在一个主机上, 就使用这个IP
client.start();
}
}
注意这里使用到了一个特殊的IP地址: "127.0.0.1" 这个IP地址叫做"回环IP", 代表"本机", 如果客户端可服务器都在同一个主机上, 就使用这个IP.
下面我们来看一下运行结果:
没有任何问题~
服务器和客户端交互的过程大致如下:
(1) 启动服务器, 服务器等待请求, 如果没有请求发来, 就一直阻塞.
(2) 启动客户端, 在客户端输入内容, 发送请求. (客户端发送完请求之后进入receive等待服务器返回响应.)
(3) 服务器收到请求, 并对请求做出响应. 服务器往客户端返回响应.
(4) 客户端收到响应, 交互结束. 进入下一轮交互.
二. TCP 回显服务器
1. TCP Echo Server
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoSever {
private ServerSocket serverSocket = null;
public TcpEchoSever(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
Socket clientSocket = serverSocket.accept();
// 每次服务器调用一次accept就会产生一个新的socket对象, 来和客户端进行"一对一服务"
// TCP建立连接的过程是由操作系统完成的, 代码不能直接感知到 ~
// 已经完成建立连接的操作了, 才能进行accept. accept相当于是针对内核中已经建立好的连接做一个"确认"动作.
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) {
// 先打印客户端信息
System.out.printf("[%s:%d] 客户端上线!", clientSocket.getInetAddress(), clientSocket.getPort());
// 获取到Socket中持有的流对象.
//TCP是全双工的通信, 一个Socket对象, 既可以读, 也可以写 ~
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
// 使用Scanner包装一下inputStream
Scanner scanner = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 1. 读取请求并解析
if (!scanner.hasNext()) {
// 如果scanner中无法读取出数据, 说明客户端关闭了连接, 导致服务器这里读到了末尾 ~
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端
//outputStream.write(response.getBytes());
printWriter.println(response); //这里使用println是为了在数据末尾能够加上一个换行.
// 4. 打印日志
System.out.printf("[%s:%d] req=%s; resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
request, response);
}
} catch(IOException e) {
e.printStackTrace();
}
System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(),clientSocket.getPort());
}
private String process(String request) {
return request;
}
}
2. TCP Echo Client
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
Scanner scannerIn = new Scanner(System.in);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
// 1. 从控制台读取数据
System.out.print("-> ");
String request = scannerIn.next();
// 2. 把请求发送给服务器
printWriter.println(request);
printWriter.flush(); // 刷新缓冲区
// 3. 从服务器读取响应
if (!scanner.hasNext()) {
break;
}
String response = scanner.next();
// 4. 打印响应结果
System.out.println(response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}