前言

  • 网络编程中,服务器的构造方法都需要手动指定端口号! 服务器手动指定的端口号是程序员可控的! 如果不指定端口号,系统自动分配端口号,在服务器中凌乱且会出现问题. 而客户端的端口号由客户端自行分配,因为客户端的端口号程序员无法得知. 0~1023端口号被用作历史著名的服务器,避开这些端口号分配端口.
  • 网络编程可分 UDPTCP 两种形式,将网络传输称为socket套接字

UDP协议代码

服务器

UDP使用数据报的形式传送数据, java标准库通过DatagramSocket类表示用于发送和接收数据报包的套接字。
DatagramPacket封装一个数据报包; 由DatagramSocket对象发送和接收.

public class UDPServer {
    private DatagramSocket socket  = null;  //由socket接收或发送数据报包裹

    public UDPServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);//手动指定服务器端口号
    }
}

↓给一个成员方法start用于调用启动服务器.这个服务器不断的循环在接收请求

通过receive()方法接收数据报包裹,receive方法没有返回值. java在设计时,没有将receive接收到的数据用 ‘=’ 赋给字节数组, 而是拿一个字节数据报包来装载,承当传递的单位,这复合了UDP特点,并且这样使得包裹可以得到重复利用
receive接收不到请求是会阻塞等待的.

DatagramPacket 的对象 可以通过request.getData()拿到字节数组, 随后我们可以将其转换成字符串读取请求

通过sent()回复请求, 需要特别注意的是,UDP 传输是无连接的, 作为回复的包裹DatagramPacket,就必须手动指定IP和端口号

  • 在UDP通信中,当客户端发送一个数据包给服务器时,客户端会将自己的IP地址和端口号放入数据包的头部(源地址和源端口)。当服务器通过receive方法接收到这个数据包时,这些信息会被自动解析并存储在接收到的DatagramPacket对象中。
void start() throws IOException {
        System.out.println("服务器启动");
        while(true){
            //构造一个包裹,给字节缓冲区,并且设置读取的最大总长度
            DatagramPacket request = new DatagramPacket(new byte[8192],8192);
            //通过循环不断的接收请求
            socket.receive(request);//将接收的请求数据放入这个包裹中(缓冲)存放
            //拿到字节数组,然后转换成String
            String requestString = new String(request.getData(),0, request.getLength());
            String responseString = process(requestString);//通过process方法来处理业务逻辑
            //像接收一样,需要一个字节包裹送出回复
            //如果你写成这样,这个是用来接收数据的,一定要指定客户端的IP和端口号
            //DatagramPacket response = new DatagramPacket(responseString.getBytes(),responseString.getBytes().length);
            //当receive接收之后,IP和端口号就存在这个缓冲区了
            DatagramPacket response = new DatagramPacket(responseString.getBytes(),responseString.getBytes().length,request.getSocketAddress());
            //将包裹回复
            socket.send(response);
            System.out.printf("客户端IP和端口号%s",request.getSocketAddress());
            Date date = new Date();
            System.out.println(" 时间:" + date);
        }
    }
//具体处理业务逻辑
   private String process(String requestString) {
        return requestString;
    }

客户端

客户端和服务器都是同用DatagramSocket
发送的接收的都是相同的. 服务器在请求的包裹中可以拿到IP地址和端口, 而客户端是先发送的一方,需要手动指定服务器IP和端口

public class UDPClient {
    private DatagramSocket socket = null;
    private String IP;
    private int Port;
    //无连接需要指定发送目的地IP和端口号
    // 此处 ip 使用的字符串, 点分十进制风格. "192.168.2.100"
    public UDPClient(String IP,int Port) throws SocketException {//任意分配端口号
        this.socket = new DatagramSocket();
        this.IP = IP;//存一下IP
        this.Port = Port;//存一下端口号
    }
    public void start() throws IOException {
        while(true){
            //构造请求
            System.out.print("输入'退出'可退出 ->");
            Scanner scanner = new Scanner(System.in);
            String requestString = scanner.next();
            if(requestString.equals("退出"))
                break;
            //将请求装进包裹里面
            //DatagramPacket request = new DatagramPacket(requestString.getBytes(),requestString.getBytes().length,IP,Port);
            //IP控制台写入是String,传出需要改类型
            DatagramPacket request = new DatagramPacket(requestString.getBytes(),requestString.getBytes().length, InetAddress.getByName(IP), Port);
            socket.send(request);//将装在包裹里面的请求发出
            //等待响应回复
            //创建一个包来装回复
            DatagramPacket response = new DatagramPacket(new byte[8192],8192);
            socket.receive(response);

            //将包裹里的字节数据转换成String
            //String responseString = new String(response.getData(),0,response.getData().length);
            String responseString = new String(response.getData(),0,response.getLength());//获取有效长度
            System.out.println(responseString);
        }
    }
}

TCP 协议代码

  • TCP 协议传输没有以什么为单位,而是以的形式来传输,因此,可以使用IO流来进行网络传输

  • TCP需要分类给服务器使用ServerSocket 是专门给服务器接收用户的,就好比如:

    1. 刚下高铁站时,有很多DD的揽客员在揽客,ServerSocket 不断在接收客户端请求
    2. 现在你有需求了,向他提出,他就专门叫了总部给你分配一个DD司机,这个DD司机就如Socket 对象,专门负责处理你的请求
  • 客户端只使用Socket类发送请求和接收回复

服务器

serverSocket对象揽客,然后分配 socket ,进入具体的流程

public class TCPFirst {
    private ServerSocket serverSocket = null;

    public void TCPServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true){  //循环接收客户端
            Socket socket = serverSocket.accept(); //没有就会阻塞等待
            System.out.println(socket.getInetAddress() + "连接成功,正在接收数据");
            processingLogic(socket);//分配司机之后处理逻辑流程
        }
    }

和之前说的一样,接收和发送都用IOStream方式
PrintWriter 调用println时先将内容放置到了缓冲区,等到内容足够多才一并发送,flush()可以手动刷新

public void processingLogic(Socket socket) throws IOException {

        //以流的方式接收请求和发送回复
        try(InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream()){
                //通过Scanner读取
            Scanner scanner = new Scanner(inputStream);
            //假设一个用户一直发送请求,那就要一直响应
            while(scanner.hasNext()){
                String requestString = scanner.next();
                String responseString = process(requestString);
                //可以这样写,但是没有换行在最后面
                //outputStream.write(responseString.getBytes(), 0, responseString.getBytes().length);
                //这里以Scanner的相对应PrintWriter写回复
                PrintWriter writer = new PrintWriter(outputStream);
                writer.println(responseString);
                writer.flush();//刷新缓冲区
            }
            System.out.printf("客户端%s关闭,port:%s\n",socket.getInetAddress(),socket.getPort());
        }
    }

这里会发现,一开始的揽客不是并发的,在学习中,使用多线程将揽客的操作改成并发执行的.
但是在实际中,一个客户一个线程显然是不行的,一旦用户占据着线程不松手,线程无法释放,就资源不足以支撑其他的客户端, 实际中的解决方案采用IO多路复用,通过IO流走多条路径回复多客户端.

 public void start(){
        System.out.println("服务器启动!");
        ExecutorService poll = Executors.newFixedThreadPool(6);//实际中不应该用线程
        // 肯定是一直在找客户
        while (true) {
                poll.submit(() -> {
                    try (Socket socket = serverSocket.accept()) {  //分配一个DD司机
                        System.out.println(socket.getInetAddress() + "连接成功,正在接收数据");
                        processingLogic(socket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                });
        }

    }

客户端

和服务器逻辑基本相同

public class TCPClient {
    private Socket socketClient = null;

    public TCPClient(String IP,int port) throws IOException {
        this.socketClient = new Socket(IP,port); //需要访问的IP和端口
    }
    public void start() throws IOException {
        System.out.println("客户端启动");
        try(InputStream inputStream = socketClient.getInputStream();
            OutputStream outputStream = socketClient.getOutputStream()){
            Scanner scanner = new Scanner(System.in);
            while(scanner.hasNext()){
                String request = scanner.next();
                PrintWriter writer = new PrintWriter(outputStream);
                writer.println(request);  //发送
                writer.flush(); //刷新缓冲区
                Scanner scannerStream = new Scanner(inputStream);  //创建网卡读入流
                String response = scannerStream.next(); //读入
                System.out.println(response);
            }

        }
    }
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐