Java EE(12)——网络编程——UDP/TCP回显服务器

发布于:2025-03-20 ⋅ 阅读:(17) ⋅ 点赞:(0)

前言

本文主要介绍UDP和TCP相关的API,并且基于这两套API实现回显服务器

UDP和TCP

UDP和TCP属于网络五层模型中传输层的协议
特点:
UDP:无连接,不可靠,面向数据包,全双工
TCP:有连接,可靠,面向字节流,全双工
1.无连接和有连接
这里所说的连接不是指物理意义上的通过实物来进行绑定,而是虚拟的连接。举个例子,你打电话给对方,对方接通之后你才能进行后续通信,这就是有连接;无连接就相当于QQ发消息,无论对方是否同意,消息都能发过去
2.可靠和不可靠
不论是哪种通信方式,实际上都无法保证数据一定能传输成功。所以,这里的可靠指的是,能获取到数据的传输情况。即使传输失败了我能知道它传输失败了,重传就是了;不可靠指的是信息传输之后就不管了,传输成功与否都和我无关,更不会重传
3.全双工和半双工
全双工通信允许通信的双方可以同时发送和接收数据,半双工通信同一时间内只能在同一方向上传输

什么是回显服务器

回显的意思是无论客户端给服务器发送什么请求,服务器会把客户端的请求原样返回

1.UCP回显服务器

1.1API介绍

Java中UDP协议的API有两个,一个是DatagramSocket,一个是DatagramPacket

1.1.1DatagramSocket类

作用:用于应用程序之间发送和接收UDP数据报
构造方法:

//不指定端口号,由系统随机分配
DatagramSocket()
//指定端口号
DatagramSocket(int port)

其他方法:

//接收一个数据报
receive(DatagramPacket p)
//发送一个数据报
send(DatagramPacket p)

1.1.2DatagramPacket类

作用:封装UDP数据报的数据和目标地址信息。它包含要发送的数据,目的主机的端口号和IP地址
构造方法:

//字节数组,字节数组长度,服务器IP,服务器端口号
//这是传入的IP地址和端口号是固定值,因为服务器的IP和端口号一般不变
//所以作为DatagramSocket类的send方法的参数,用于客户端向服务器发送请求
DatagramPacket(byte buf[], int length,InetAddress address, int port)

//字节数组,字节数组长度
//这个和上面有所不同,主要用于服务器向客户端返回请求
//一会代码再具体讲解
DatagramPacket(byte buf[], int length, SocketAddress address)

//字节数组,字节数组长度
//作为DatagramSocket类的receive方法的参数,用于客户端接收服务器的响应   或者   用于服务器接收客户端的请求
DatagramPacket(byte buf[], int length)

1.2UDP回显服务器代码

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class EchoSever {
    private final DatagramSocket socket;
    //传入端口号
    public EchoSever(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动");
        //服务器死循环就行了,想要关闭服务器直接杀进程
        while (true) {
            //1.读取请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //receive从网卡读取到UDP数据报,载荷部分放到byte数组里
            //UDP报头存放在requestPacket其他属性里
            //还能通过requestPacket知道源IP/端口
            //receive具有阻塞功能
            socket.receive(requestPacket);
            //将接收到的字节数组转换为字符串
            //requestPacket.getLength()得到的长度是有效长度,不一定是4096
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2.根据请求计算相应(回显服务器啥都不用做)
            String response = process(request);
            //3.返回响应到客户端
            //response.getBytes()得到String内部的字节数组
            //当String里面全都是英文字符的时候,response.length()是可以的
            //因为一个英文字母对于一个字节,但是一个汉字对应多个字节
            //requestPacket.getSocketAddress()找到对应客户端的IP和端口号
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //打印日志
            //IP,端口号,请求,响应
            System.out.printf("[%s:%d] request:%s,response:%s\n",requestPacket.getAddress().toString(),
                    responsePacket.getPort(),request,response);
        }
    }
    //根据请求计算相应
    public String process(String request){
        return request;
    }
    //
    public static void main(String[] args) throws IOException {
        EchoSever sever = new EchoSever(9090);
        sever.start();
    }
}

注意1:DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length,requestPacket.getSocketAddress());
因为requestPacket数据报保存了客户端的IP地址和端口号,所以通过getSocketAddress()能获取到客户端的IP地址和端口号
//
其次,客户端的IP地址和端口号是系统随机分配的,而且服务器会同时处理多个客户端请求,这些客户端的IP和端口号都不一样,通过getSocketAddress()才能正确的找到对应客户端,而不是输入固定的IP和端口号
//
这就是为什么该方法用于服务器返回客户端的响应,而DatagramPacket(byte buf[], int length,InetAddress address, int port)用于客户端向服务器发送请求
//
对比两个方法不难发现,前者获取到的IP和端口号是不固定的,而后者是指定IP和端口号
注意2:为什么服务器要指定端口号?而客户端的端口号是随机分配?
讲服务器比作餐厅,客户端比作顾客。顾客来餐厅吃饭,坐的桌子就相当于端口号,顾客今天坐001号桌,改天坐002号桌,人家乐意坐哪就坐那;但是餐厅的位置肯定是固定的,不可能今天餐厅在河边,明天餐厅就跑到半山腰去了吗,餐厅的位置不能改变,否则顾客找不到餐厅的位置

1.2UDP客户端代码

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class EchoClient {
    private final DatagramSocket socket;
    //这里的IP是String
    private final String severIP;
    private final int severPort;
    //传入服务器IP和端口
    public EchoClient(String severIP,int severPort) throws SocketException {
        this.socket = new DatagramSocket();
        this.severIP = severIP;
        this.severPort = severPort;
    }
    //启动客户端
    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner in = new Scanner(System.in);
        while (true){
            //提示用户要输入请求了
            System.out.print("-> ");
            //1.从控制台读取要发送的请求数据
            //在用户输入之前有阻塞效果
            if (!in.hasNext()){
                break;
            }
            String request = in.next();
            //2.请求并发送
            //字节数组,字节数组长度,服务器IP,服务器端口号
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(severIP),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 {
        EchoClient client = new EchoClient("127.0.0.1",9090);
        client.start();
    }
}

2.TCP回显服务器

2.1API介绍

Java中TCP协议的API有两个,一个是SeverSocket,一个是Socket

2.1.1SeverSocket类

作用:用于服务器监听来自客户端的TCP连接请求
构造方法:

//不指定端口号,由系统随机分配
ServerSocket()
//指定端口号
ServerSocket(int port)

其他方法:

//监听并接受客户端传入的连接请求。此方法有阻塞效果,直到有客户端连接
Socket accept():

2.1.2Socket类

作用:主要用于客户端和服务器之间建立TCP连接
构造方法:

//通过传入IP和端口号连接到指定的主机(服务器)
Socket(String host, int port)

其他方法:

//返回此套接字(实例)的输入流,用于接收数据
getInputStream()
//返回此套接字(实例)的输出流,用于发送数据
getOutputStream()

2.1TCP回显服务器代码

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 final ServerSocket socket;
    //
    public TCPEchoSever(int port) throws IOException {
        socket = new ServerSocket(port);
    }
    //启动服务器
    private void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            //将服务器和客户端连接
            //accept()有阻塞效果,等待客户端建立联系
            Socket clientSocket = socket.accept();
            //每与一个客户端建立连接,都创建一个线程来执行客户端的请求
            Thread thread = new Thread(()->{
                //服务器和客户端交互
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();
        }
    }
    //
    public void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 服务器上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
            //inputStream从网卡读数据
        try(InputStream inputStream = clientSocket.getInputStream();
            //OutputStream往网卡写数据
            OutputStream outputStream = clientSocket.getOutputStream()) {
            //从网卡读数据
            //byte[] array = new byte[1024];int ret = inputStream.read(array);
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner scanner = new Scanner(inputStream);
            while (true){
                //读取完毕,当客户端下线的时候产生
                //在用户输入之前,hasNext()有阻塞效果
                //当客户端断开连接时,scanner.hasNext()返回false并中断循环
                if (!scanner.hasNext()){
                    System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //1.读取请求并解析
                //用户传过来的请求必须带有空白符,没有的话就会阻塞
                String request = scanner.next();
                //2.计算响应
                String response = process(request);
                //3.返回响应
                //outputStream.write(response.getBytes(),0,response.getBytes().length);//这个方式不方便添加空白符
                //通过PrintWriter来封装outputStream
                //添加\n
                printWriter.println(response);
                //刷新缓冲区
                printWriter.flush();
                //打印日志
                System.out.printf("[%s:%d] request:%s,response:%s\n",clientSocket.getInetAddress(),
                        clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }
    }
    //计算响应
    private String process(String request) {
        return request;
    }
    //
    public static void main(String[] args) throws IOException {
        TCPEchoSever sever = new TCPEchoSever(9090);
        sever.start();
    }
}

注意:为什么要调用clientSocket.close()?
因为每和一个客户端连接都会创建一个clientSocket套接字,它负责和客户端交互,即便客户端进程终止了,客户端的socket会被操作系统回收,但服务器中的clientSocket仍然会占用文件描述符和内存资源。当文件资源耗尽时,就无法与新的客户端建立连接

2.2TCP客户端代码

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 final Socket socket;
    public TCPEchoClient(String severIp,int port) throws IOException {
    	//与服务器建立联系
        socket = new Socket(severIp,port);
    }
    //
    public void start(){
        System.out.println("客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            //读取控制台
            Scanner scannerConsole = new Scanner(System.in);
            Scanner scannerNetWork = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                System.out.print("->");
                //在用户输入之前,hasNext()有阻塞效果
                if (!scannerConsole.hasNext()){
                    break;
                }
                //1.从控制台输入请求
                String request = scannerConsole.next();
                //2.发送请求
                //让请求的结尾有\n
                printWriter.println(request);
                //刷新缓冲区
                printWriter.flush();
                //3.从服务器读取响应
                String response = scannerNetWork.next();
                //4.将响应打印到控制台
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    //
    public static void main(String[] args) throws IOException {
        TCPEchoClient client = new TCPEchoClient("127.0.0.1",9090);
        client.start();
    }
}