网络编程

什么是网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)

Socket套接字

网络编程的核心就是 Socket API (操作系统给应用程序提供网络编程的 API)

可以认为是Socket API 是跟传输层密切相关的, 传输层里提供了最核心的两种协议, (UDP, TCP), 因此Socket API 也提供了两种风格(UDP,TCP)

基于 UDP 编写一个客户端服务器网络通信程序

DatagramSocket
使用这个类来表示 Socket 对象, 在操作系统中, 把这个Socket对象也是当成一个文件来处理, 相当于是文件描述符上的一项
普通的文件, 对应的硬件设备是硬盘,
Socket 文件, 对应的硬件设备是网卡

一个 Socket 对象, 就可以和另外一个主机通信了. 要想和多个不同的主机进行通信就需要创建多个 Socket 对象

DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:

上述没有参数的版本, 就是没有指定端口, 系统则会自动分配一个空闲的端口
有参数的版本是要传入一个端口, 此时就是让 Socket 对象和这个端口关联起来,
本质上说, 不是进程和端口之间建立联系, 是进程中的 Socket 对象和端口建立了联系

DatagramSocket 方法:

receive 这个方法此处传入的相当于一个空的对象, receive 方法内部会对这个空对象进行填充, 从而构造出结果数据, 参数也是一个"输出型参数"
close 这个方法是释放资源的, 用完之后进行关闭

DatagramPacket
DatagramPacket 表示 UDP 传输的一个报文
DatagramPacket 构造方法:

编写一个最简单的 UDP 版本的客户端, 服务器程序----回显示服务器(echo server)

一个普通的服务器是: 收到请求, 根据请求计算响应, 返回响应
我们这里省略了计算的过程, 也就是请求什么就返回什么, (这个代码没有什么实际业务, 只是展示一下 Socket API 的用法)
作为一个真正的服务器"根据请求计算响应是及其重要的"

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

// UDP 版本的回显服务器
public class UdpEchoServer {
    // 网络编程, 本质上是要操作网卡
    // 但是网卡不方便直接操作, 在操作系统内核中, 使用"Socket"这样的文件来抽象表示网卡
    private DatagramSocket socket = null;

    // 对于服务器来说, 创建 Socket 对象的同时, 要让他绑定一个端口号
    // 服务器一定要关联一个具体的端口, 服务器是网络传输中被动的一方,
    // 如果是操作系统随机分配一个端口, 客户端就不知道这个端口是啥了, 也就无法通信

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        // 服务器不只是只给一个客户端提供服务, 是需要服务很多客户端的
        while (true) {
            // 只要有客户端过来就提供服务
            // 1. 读取服务端发过来的请求
            // receive 方法的参数是一个输出型参数, 需要先构造好一个空白的 DatagramPacket 对象, 发给receive 进行填充
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            // receive 内部会针对参数对象进行填充, 填充的数据来自于网卡
            socket.receive(requestPacket);
            // 此时DatagramPacket 是一个特殊的对象. 不好处理, 可以把这里包含的数据拿出来, 构造一个字符串
            // 此处给的最大长度是4096, 但是这里的这些空间不一定用满了, 可能只用了一小部分
            // 因此构造字符串的时候, 就通过getLength 来获取实际的数据报长度,
            // 把这个实际有小部分构造成字符串即可
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());

            // 2.根据请求计算响应
            String response = process(request);

            // 3. 把响应写回客户端, sent 的参数也是DatagramPacket, 需要把Packet对象构造好
            // 此处构造的响应对象, 不能用空的字节数组构造, 需要使用响应数据来构造
            // 获取到客户端的IP和端口号(这两个信息本来就在 requestPacket中)
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            // 4. 打印日志, 当前这次请求响应的处理中间结果
            // 参数分别是 Packet 里的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 {
        // 端口号(1024 ~ 65535)
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

对于我们这个程序, 这里的while是得一直循环的, 这样的死循环在服务器程序中是没啥问题.一般服务器是7*24 小时运行的

UDP 版本的回显客户端

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

// UDP 版本的回显客户端
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp = null;
    private int serverPort = 0;

    // 构造这个 socket 对象, 不需要显示绑定一个端口(系统自动分配)
    // 一次通信需要两个ip, 两个端口
    // 客户端的 ip 是 127.0.0.1
    // 服务器 ip 和 端口 也要高数客户端, 才能正确把消息发给服务器
    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 = new Scanner(System.in);

        while (true) {
            // 1. 从控制台读取要发送的数据
            System.out.print("> ");
            String request = scanner.next();
            if (request.equals("exit")) {
                System.out.println("goodbye");
                break;
            }
            // 2. 构造UDP请求, 发送
            // 构造这个Packet的时候, 需要把 serverIp和 port 都传过来, 但是此处IP地址需要填写的是一个32位的整数形式
            // 上述的IP地址是一个字符串, 需要使用InetAddress.getByName 来进行一个转换
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);
            // 3. 读取UDP服务器响应并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0, responsePacket.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();
    }
}

网络编程套接字-LMLPHP
网络编程套接字-LMLPHP
我们在基于上面的代码, 实现一个查词典的功能

/**
 * @describe
 * @author chenhongfei
 * @version 1.0
 * @date 2023/10/24
 */
package network;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

// 对于DictServer, 和EchoServer 相比, 大部分东西都是一样的
// 主要是对于根据请求计算响应的代码进行修改
public class UdpDictServer extends UdpEchoServer{
    private Map<String,String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        // 给这个dict设置内容
        dict.put("dog","狗");
        dict.put("cat","猫");
        dict.put("hello","你好");
        // .....
    }

    public String process(String request) {
        // 查词典的过程
        return dict.getOrDefault(request,"当前单词没有查询到结果");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9090);
        server.start();

    }

}

网络编程套接字-LMLPHP
如果我们多个客户端都使用同一个端口, 就会出现异常
网络编程套接字-LMLPHP

基本过程:

  1. 服务器先启动, 运行到 receive 阻塞
  2. 客户端开始执行, 客户端开始读取用户 输入的内容
  3. 客户端发送请求
  4. 客户端在这里 进行阻塞等待服务器响应, 接着服务器接受请求,
  5. 根据请求计算响应
  6. 执行send 返回响应,
  7. 最后客户端从阻塞中返回,读到响应了

TCP流套接字编程

TCP 提供的API主要是两个类
SeverSocket 专门给服务器使用的 Socket 对象
Socket 既可以给客户端使用, 也可以给服务器使用

ServerSocket API

ServerSocket 是创建TCP服务端Socket的API
ServerSocket 构造方法:

ServerSocket 方法:

Socket 方法:

TCP 回显服务器

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动 !");
        // 此处使用这个动态变化的线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true) {
            // 使用这个clientSocket 和具体的客户端进行交流
            Socket clientSocket = serverSocket.accept();
//            Thread t = new Thread(() -> {
                // 多线程版本
                // 这里最大的问题就是频繁的申请释放资源
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    throw new RuntimeException(e);
//                }
//            });
//            t.start();
                // 使用线程池来解决
            threadPool.submit(() -> {
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

    // 使用这个方法来处理连接
    // 这一个连接对应到一个客户端, 这里可能会涉及到多次交互
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());
        // 继续上述的Socket对象和客户端通行
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 因为要处理多个请求和响应, 也是要循环来进行
            while (true) {
                // 1. 读取目录
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNextInt()) {
                    // 没有数据了, 就说明读完了(客户端关闭连接)
                    System.out.printf("[%s:%d]客户端下线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 此处使用 next 是一直读取到 换行符/空格/其他空白符结束. 但是最终结果不返回空白符
                String request = scanner.next();
                // 2. 根据请求构造响应
                String response = process(request);
                // 3. 返回响应结果
                // outputStream 没有write string 这样的功能, 可以把String 里的字节数组拿出来, 进行写入
                // 也可以用字符流来转换一下
                PrintWriter printWriter = new PrintWriter(outputStream);
                // 此处使用 println 来写入, 让结果有一个\n 换行, 方便对端接受解析
                printWriter.println(request);
                // flush 刷新缓冲区, 保证当前写入的数据, 确实发送出去了
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                        request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(4090);
        server.start();
    }
}

网络编程套接字-LMLPHP

TCP 回显客户端

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

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // Socket 构造方法, 能够识别点分十进制的IP地址, 比 DatagramPacket 更方便
        // new 这个对象的时候就会进行 TCP 连接操作
        socket = new Socket(serverIp,serverPort);
    }
    public void start() {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                // 1. 先从键盘上读取用户输入的内容
                System.out.print("> ");
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("goodbye");
                    break;
                }
                // 2. 把读到的内容构成请求, 发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                // 此处加上close 确保数据发送出去
                printWriter.flush();
                // 3. 读取服务器的响应
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();
                // 4. 把响应的内容显示到界面上
                System.out.println(request);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",4090);
        client.start();
    }
}
10-27 11:25