java中的socket(二)

497 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

在前面一篇文章中谈到了socket的一些基本知识:java基础之socket(一) ,今天谈谈socket编程。socket通信方式有两种:一种是数据报通信,另一种就是流通信。

数据报通信协议:即UDP(User Data Protocol 用户数据报协议)。UDP是一种无连接的协议,这就意味着每次发送数据报时,需要同时发送本机的socket描述符和接收端的socket描述符。因此,在每次通信时都需要发送额外的数据。

流通信协议:也叫做TCP(Transfer Control Protocol,传输控制协议)。和UDP不同,TCP是一种基于连接的协议。在使用流通信之前,必须在通信的一对儿socket之间建立连接。其中一个socket作为服务器进行监听连接请求。另一个则作为客户端进行连接请求。一旦两个socket建立好了连接,就可以单向或双向进行数据传输。

UDP与TCP

在UDP中,每次发送数据报时,需要附带上本机的socket描述符和接收端的socket描述符。而由于TCP是基于连接的协议,在通信的socket对之间需要在通信之前建立连接,因此会有建立连接这一耗时存在于TCP协议的socket编程。

在UDP中,数据报数据在大小上有64KB的限制。而TCP中也不存在这样的限制。一旦TCP通信的socket对建立了连接,他们之间的通信就类似IO流,所有的数据会按照接受时的顺序读取。

UDP是一种不可靠的协议,发送的数据报不一定会按照其发送顺序被接收端的socket接受。然后TCP是一种可靠的协议。接收端收到的包的顺序和包在发送端的顺序是一致的。

TCP适合于诸如远程登录和文件传输这类的网络服务。而UDP相比TCP更加简单轻量一些。UDP用来实现实时性较高或者丢包不重要的一些服务。

socket通信实现

客户端

开启两个进程,一个从控制台中读取数据,读取到用户输入时即向服务端发送读取到的数据,把它称为写进程。另一个接收服务端发送的数据,并把它显示到控制台上,称为读进程。

public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        Socket socket = new Socket("169.10.101.1", 9000);

        PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        
        // 写线程
        new Thread(() -> {
            // 循环从控制台读取数据
            String message = "";
            while (!message.equals("exit")) {
                message = scanner.nextLine();
                writer.println(message);
                writer.flush();
            }
            writer.close();
        }).start();

        // 读线程
        new Thread(() -> {
            String line = "";
            try {
                while ((line = bufferedReader.readLine()) != null) {
                    System.out.format("%50s", line);
                    System.out.println();
                }
                bufferedReader.close();
            } catch (IOException e) {

            }
        }).start();

    }

服务端

为了响应多个客户端连接,需要在循环中不停的调用accept()方法,每当获取到一个新TCP连接时,把获取到的Socket对象存入set中,对每个Socket对象都开启一个线程,主要的任务是不停的从InputStream中读取数据,即接收发送客户端数据。获取到数据后,再发送给所有已连接的客户端Socket,即遍历set发送数据。

public class Server {
    public static int userCount = 0;

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9000);
        Set<Socket> socketSet = new HashSet<>();

        while (true) {
            Socket socket = serverSocket.accept();
            socketSet.add(socket);
            userCount++;

            Thread thread = new Thread(() -> {
                // 不停从input流获取数据
                BufferedReader bufferedReader = null;
                try {
                    bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String userName = Thread.currentThread().getName();
                    String line = "";
                    System.out.println("开启读线程");
                    while ((line = bufferedReader.readLine()) != null) {
                        System.out.println(userName + line);
                        for (Socket tem :
                                socketSet) {
                            if (!tem.equals(socket)) {
                                PrintWriter writer = new PrintWriter(new OutputStreamWriter(tem.getOutputStream()));
                                writer.println(line + ":" + userName);
                                writer.flush();
                            }
                        }
                    }
                    bufferedReader.close();
                } catch (IOException e) {
                }
                System.out.println("读完成");
            });
            thread.setName("用户" + userCount);
            thread.start();
        }

    }
}

需要注意的是:要告知对方已发送完命令,如果客户端打开一个输出流,不做约定也不关闭它,那么服务端永远不知道客户端是否发送完消息,服务端会一直等待下去,直到读取超时。所以怎么告知服务端已经发送完消息就显得特别重要。

方法一:通过Socket关闭输出流的方式。当Socket关闭的时候,服务端就会收到响应的关闭信号,那么服务端也就知道流已经关闭了,这个时候读取操作完成。但是这种方式有一些缺点:客户端Socket关闭后,将不能接受服务端发送的消息,也不能再次发送消息。如果客户端想再次发送消息,需要重现创建Socket连接。

方法二:通过Socket关闭输出流的方式这种方式调用的方法是:

socket.shutdownOutput();

而不是下面这样:

outputStream.close();

调用Socket的shutdownOutput()方法,底层会告知服务端已经写完了,服务端收到消息后,就能知道已经读取完消息,如果服务端有要返回给客户的消息那么就可以通过服务端的输出流发送给客户端,如果没有,直接关闭Socket。但是还是有点问题:不能再次发送消息给服务端,如果再次发送,需要重新建立Socket连接。这个缺点,在访问频率比较高的情况下将是一个需要优化的地方。