Java socket编程学习笔记

一、初步了解

1、简易代码(存在socket提前关闭问题)

服务端代码:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class MySocketServer {
    public static void main(String[] args) throws IOException {
        // 创建一个serverSocket,监听一个端口号并创建通信socket
        ServerSocket serverSocket = new ServerSocket(8888);
        // 当有客户端连接时创建一个通信socket,没有连接时会阻塞
        Socket socket = serverSocket.accept();
        // 打印客户端信息
        System.out.println("客户端" + socket.getInetAddress().getLocalHost() + "连接到服务器");
        // 输入流,用于读取客户端信息
        InputStream in = socket.getInputStream();
        // 良好习惯,关闭输出流
        in.close();
        // 输出流,用于给客户端返回信息
        OutputStream out = socket.getOutputStream();
        /* 读取客户端信息 */
        byte[] buffer = new byte[1024];
        int len;
        StringBuilder msgBuilder = new StringBuilder();
        while ((len = in.read(buffer)) != -1) {
            msgBuilder.append(new String(buffer, 0, len));
        }
        // 打印从客户端收到的信息
        System.out.println("服务端接收到消息:" + msgBuilder.toString());
        // 给客户端返回信息
        out.write(("服务端收到消息:" + msgBuilder.toString()).getBytes(StandardCharsets.UTF_8) );
        // 输出缓冲数据
        out.flush();
        // 关闭输出流
        out.close();
    }
}

客户端代码
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Date;

public class MySocketClient {
    public static void main(String[] args) throws IOException {
        // 指定ip和端口号,创建socket连接
        Socket socket = new Socket("127.0.0.1", 8888);
        /* 发送消息 */
        String msg = "客户端发送了一条消息,现在是北京时间" + new Date();
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        out.write(msg.getBytes(StandardCharsets.UTF_8));
        out.flush();
        // 关闭输出流
        out.close();
        /* 读取信息 */
        byte[] buffer = new byte[1024];
        int len;
        StringBuilder msgBuilder = new StringBuilder();
        while ((len = in.read(buffer)) != -1) {
            msgBuilder.append(new String(buffer, 0, len));
        }
        // 打印收到信息
        System.out.println("客户端收到服务端回信:" + msgBuilder);
        // 关闭输入流
        in.close();
    }
}

运行结果

  1、步骤:先启动服务端,再启动客户端
  2、服务端打印:

客户端LAPTOP-EECN3AOI/192.168.31.39连接到服务器
Exception in thread "main" java.net.SocketException: Socket is closed
	at java.net.Socket.getOutputStream(Socket.java:943)
	at MySocketServer.main(MySocketServer.java:19)

 3、客户端打印:

Exception in thread "main" java.net.SocketException: socket closed
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
	at java.net.SocketInputStream.read(SocketInputStream.java:171)
	at java.net.SocketInputStream.read(SocketInputStream.java:141)
	at java.net.SocketInputStream.read(SocketInputStream.java:127)
	at MySocketClient.main(MySocketClient.java:24)

  4、错误分析:socket被关闭
  5、原因:分析代码,未对socket进行关闭,但是客户端在通信完成前提前关闭了out流,服务端提前关闭了in流,查询资料得知关闭流会导致socket关闭

2、将所有关闭流操作去掉,存在读取阻塞问题

服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class MySocketServer {
    public static void main(String[] args) throws IOException {
        // 创建一个serverSocket,监听一个端口号并创建通信socket
        ServerSocket serverSocket = new ServerSocket(8888);
        // 当有客户端连接时创建一个通信socket,没有连接时会阻塞
        Socket socket = serverSocket.accept();
        // 打印客户端信息
        System.out.println("客户端" + socket.getInetAddress().getLocalHost() + "连接到服务器");
        // 输入流,用于读取客户端信息
        InputStream in = socket.getInputStream();
        // 输出流,用于给客户端返回信息
        OutputStream out = socket.getOutputStream();
        /* 读取客户端信息 */
        byte[] buffer = new byte[1024];
        int len;
        StringBuilder msgBuilder = new StringBuilder();
        while ((len = in.read(buffer)) != -1) {
            msgBuilder.append(new String(buffer, 0, len));
        }
        // 打印从客户端收到的信息
        System.out.println("服务端接收到消息:" + msgBuilder.toString());
        // 给客户端返回信息
        out.write(("服务端收到消息:" + msgBuilder.toString()).getBytes(StandardCharsets.UTF_8) );
    }
}

客户端
public class MySocketClient {
    public static void main(String[] args) throws IOException {
        // 指定ip和端口号,创建socket连接
        Socket socket = new Socket("127.0.0.1", 8888);
        /* 发送消息 */
        String msg = "客户端发送了一条消息,现在是北京时间" + new Date();
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        out.write(msg.getBytes(StandardCharsets.UTF_8));
        out.flush();
        /* 读取信息 */
        byte[] buffer = new byte[1024];
        int len;
        StringBuilder msgBuilder = new StringBuilder();
        while ((len = in.read(buffer)) != -1) {
            msgBuilder.append(new String(buffer, 0, len));
        }
        // 打印收到信息
        System.out.println("客户端收到服务端回信:" + msgBuilder);
    }
}
运行结果

 1、服务端:打印连接信息后无响应

客户端LAPTOP-EECN3AOI/192.168.31.39连接到服务器

  2、客户端:无响应
  3、原因:分析代码,服务端没有打印出客户端发送消息,猜测是以下代码陷入死循环。

        byte[] buffer = new byte[1024];
        int len;
        StringBuilder msgBuilder = new StringBuilder();
        while ((len = in.read(buffer)) != -1) {
            msgBuilder.append(new String(buffer, 0, len));
        }

  查资料得知,在普通流当中,这个方法可行。但是在socket中,只有当对方将输出流关闭后才会以-1作为结束标志,故而陷入死循环,推断正确。
  4、解决方法:需要关闭流的同时,不关闭socket,可使用Socket::shutdownOutput()方法和Socket::shutdownInput()方法实现

3、使用正确方法关闭流,通信成功

服务端
public class MySocketServer {
    public static void main(String[] args) throws IOException {
        // 创建一个serverSocket,监听一个端口号并创建通信socket
        ServerSocket serverSocket = new ServerSocket(8888);
        // 当有客户端连接时创建一个通信socket,没有连接时会阻塞
        Socket socket = serverSocket.accept();
        // 打印客户端信息
        System.out.println("客户端" + socket.getInetAddress().getLocalHost() + "连接到服务器");
        // 输入流,用于读取客户端信息
        InputStream in = socket.getInputStream();
        // 输出流,用于给客户端返回信息
        OutputStream out = socket.getOutputStream();
        /* 读取客户端信息 */
        byte[] buffer = new byte[1024];
        int len;
        StringBuilder msgBuilder = new StringBuilder();
        while ((len = in.read(buffer)) != -1) {
            msgBuilder.append(new String(buffer, 0, len));
        }
        // 关闭输入流
        socket.shutdownInput();
        // 打印从客户端收到的信息
        System.out.println("服务端接收到消息:" + msgBuilder.toString());
        // 给客户端返回信息
        out.write(("服务端收到消息:" + msgBuilder.toString()).getBytes(StandardCharsets.UTF_8) );
        // 关闭输出流同时关闭socket
        out.close();
    }
}

客户端

public class MySocketClient {
    public static void main(String[] args) throws IOException {
        // 指定ip和端口号,创建socket连接
        Socket socket = new Socket("127.0.0.1", 8888);
        /* 发送消息 */
        String msg = "客户端发送了一条消息,现在是北京时间" + new Date();
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        out.write(msg.getBytes(StandardCharsets.UTF_8));
        out.flush();
        // 关闭输出流
        socket.shutdownOutput();
        /* 读取信息 */
        byte[] buffer = new byte[1024];
        int len;
        StringBuilder msgBuilder = new StringBuilder();
        while ((len = in.read(buffer)) != -1) {
            msgBuilder.append(new String(buffer, 0, len));
        }
        // 打印收到信息
        System.out.println("客户端收到服务端回信:" + msgBuilder);
        // 关闭输入流同时关闭socket
        in.close();
    }
}
输出结果

  1、服务端

客户端LAPTOP-EECN3AOI/192.168.31.39连接到服务器
服务端接收到消息:客户端发送了一条消息,现在是北京时间Sun Jan 07 16:46:26 GMT+08:00 2024

  2、客户端

客户端收到服务端回信:服务端收到消息:客户端发送了一条消息,现在是北京时间Sun Jan 07 16:46:26 GMT+08:00 2024

4、心得

(1)关闭socket的 输入/输出流 时,会将socket连接一起关闭,当socket还需要继续工作时,需要使用内置方法shutdownOutput和shutdownInput关闭流
(2)输入流只有当对方输出流关闭时,才会以-1作为结束标志

二、多次通信

  之前的代码,只进行了一次通信就将输出流关闭,实际应用中会存在多次通信,需要进行进一步优化。要进行多次通信,就需要制定每次发送消息的结束标识,有以下两种方式可以实现:
(1) 仅使用字符通信,制定一个结束标识,比如 “END” 字符串。当读取到"EDN"字符串时,就说明读取到了一个完整的消息,但有一个弊端,就是消息里可能存在与结束标识同样的内容,会干扰消息的接收,解决方法可以是在与结束标识相同内容前,加入转义字符,但处理起来比较麻烦,因此本次不此本方法实践
(2)在消息头部,加入n个字节,用于表示本次发送消息大小。通信双方约定好这个规范,接收方先读取前n个字节获取大小,再从输入流中读取对应数目的字节,就可以读取到对应的消息。本次实践使用此方法

1、第一版代码,存在信息缺漏问题

客户端代码
    public static void main(String[] args) throws IOException {
        // 指定ip和端口号,创建socket连接
        Socket socket = new Socket("127.0.0.1", 8888);
        /* 发送消息 */

        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        for (int i = 1; i <= 3; i++) {
            String msg = "客户端发送第"+ i +"条消息,现在是北京时间" + new Date();
            byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);
            // 发送本次消息长度
            out.write(msgBytes.length);
            // 发送本次消息
            out.write(msgBytes);
        }
        out.flush();
        socket.shutdownOutput();
        // 关闭输入流同时关闭socket
        in.close();
        socket.close();
    }
服务端代码
public static void test01() throws IOException{
        // 创建一个serverSocket,监听一个端口号并创建通信socket
        ServerSocket serverSocket = new ServerSocket(8888);
        // 当有客户端连接时创建一个通信socket,没有连接时会阻塞
        Socket socket = serverSocket.accept();
        // 打印客户端信息
        System.out.println("客户端" + socket.getInetAddress().getLocalHost() + "连接到服务器");
        // 输入流,用于读取客户端信息
        InputStream in = socket.getInputStream();
        // 初始化大于消息最大字节数数组,可以复用
        byte[] buffer = new byte[1024];
        while (true) {
            // 读取第一个字节获取消息长度
            int len = in.read();
            // 当客户端关闭输出流时停止读取
            if (len == -1) break;
            // 读取数据
            int readLen = in.read(buffer);
            if (readLen == -1) break;
            // 打印读取到的数据
            System.out.println(new String(buffer, 0, len));
        }
        // 关闭输入流
        socket.shutdownInput();
        socket.close();
    }
运行结果:部分消息缺漏
客户端LAPTOP-EECN3AOI/192.168.31.39连接到服务器
客户端发送第1条消息,现在是北京时间Sun Jan 14 12:54:57 GMT+08:00 2024
客户端发送第2条消息,现在是北京时间Sun Jan 14 12:54:57 GMT+08:00 2024
原因分析

 由于TCP是可靠传输,因此可以排除消息传输丢失的可能性,那么问题就出现在服务端读取上面:

while (true) {
            // 读取第一个字节获取消息长度
            int len = in.read();
            // 当客户端关闭输出流时停止读取
            if (len == -1) break;
            // 读取数据
            int readLen = in.read(buffer);
            if (readLen == -1) break;
            // 打印读取到的数据
            System.out.println(new String(buffer, 0, len));
}

 在读取完一条消息的大小后,将输入流中数据全部读入数组,而实际上此时后续消息可能也已经到达,也一并被读取到buffer中,而打印数据的代码只截取了一条消息的大小的数据,剩余后续数据在下次读取中被舍弃,因此出现了信息缺漏
 解决方法,应该在读取进buffer时只截取对应长度的数据,再将数据输出

2、第二版代码

 只对服务端进行修改,客户端代码保持和之前一致

服务端代码
    public static void test02() throws IOException {
        // 创建一个serverSocket,监听一个端口号并创建通信socket
        ServerSocket serverSocket = new ServerSocket(8888);
        // 当有客户端连接时创建一个通信socket,没有连接时会阻塞
        Socket socket = serverSocket.accept();
        // 打印客户端信息
        System.out.println("客户端" + socket.getInetAddress().getLocalHost() + "连接到服务器");
        // 输入流,用于读取客户端信息
        InputStream in = socket.getInputStream();
        // 初始化大于消息最大字节数数组,可以复用
        byte[] buffer = new byte[1024];
        int len;
        // 每次读取一字节获取消息长度,直至客户端关闭输出流
        while ((len = in.read()) != -1) {
            // 只读取消息长度的数据
            int readLen = in.read(buffer, 0, len);
            // 客户端关闭输出流时停止读取
            if (readLen == -1) break;
            System.out.println(new String(buffer, 0, len));
        }
        // 关闭输入流
        socket.shutdownInput();
        socket.close();
    }
运行结果

 读取到所有数据:

客户端LAPTOP-EECN3AOI/192.168.31.39连接到服务器
客户端发送第1条消息,现在是北京时间Sun Jan 14 13:14:30 GMT+08:00 2024
客户端发送第2条消息,现在是北京时间Sun Jan 14 13:14:30 GMT+08:00 2024
客户端发送第3条消息,现在是北京时间Sun Jan 14 13:14:30 GMT+08:00 2024

你可能感兴趣的:(java,学习,笔记)