Java网络编程(五)——ServerSocket(二)

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

在上一篇博客Java网络编程(四)—— ServerSocket(一) - 掘金 (juejin.cn)介绍了什么是服务端Socket以及如何使用服务端Socket,这篇博客继续介绍ServerSocket其他的知识点。

请求队列长度

当服务器进程运行时,可能会同时监听到多个客户的连接请求。例如,每当一个客户进程执行以下代码:

Socket socket = new Socket("123.345.746.2", 80);
复制代码

就意味着在远程123.345.746.2主机的80端口上,监听到了一个客户的连接请求。管理客户连接请求的任务是由操作系统来完成的,操作系统把这些连接请求存储在一个先进先出的队列中。许多操作系统限定了队列的最大长度,一般为50。当队列中的连接请求达到了队列的最大容量时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过ServerSocket的 accept()方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。

对于客户进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户与服务器的连接建立成功,客户进程从Socket构造方法中正常返回。如果客户进程发出的连接请求被服务器拒绝,Socket构造方法就会抛出 ConnectionException。

ServerSocket构造方法的 backlog 参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。值得注意的是,如果backlog的值设为0或者值大于操作系统限定的队列的最大长度或者没有该参数,backlog 仍然会采用操作系统限定的队列的最大长度。

接受和关闭与客户端的连接

ServerSocket 的 accept()方法从连接请求队列中取出一个客户的连接请求,然后创建与客户连接的Socket对象,之后服务器会从Socket对象中获得输入流和输出流,就能与客户交换数据。但是当服务器正在进行发送数据的操作时,如果客户端断开了连接,那么服务器端会抛出一个IOException的子类SocketException异常,那么服务器就会退出程序,因此我们需要捕获这种异常,使得服务器能够继续与其他客户端进行通信。

可以把与单个客户端通信的代码放入一个try块中,如果遇到异常,那么这个异常会被catch代码块捕获。try代码块应该再跟上一个finally代码块,用来关闭Socket,断开与该客户端的连接。

public void server()
    {
        while(true)
        {
            Socket socket = null;
            try {
                socket = serverSocket.accept();//等待客户连接
                System.out.println("已有客户端连接,地址:"+socket.getInetAddress()+" 端口号:"+socket.getPort());

                PrintWriter writer = this.getWriter(socket);
                BufferedReader reader = this.getReader(socket);
                String msg = null;
                while ((msg = reader.readLine())!=null)
                {
                    System.out.println(socket.getInetAddress()+" "+socket.getPort()+" 发来的消息:"+msg);
                    writer.println("server收到了: " + msg);
                }
            } catch (IOException e) {
            	//可以选择打印异常,也可以不打印
                //e.printStackTrace();
            }finally {
                try {
                    if(socket!=null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
复制代码

在这里插入图片描述

多线程服务器

一般的Server接收到一个客户连接,就会与该客户进行通信,通信完毕后断开连接,然后再接收下一个客户连接。但是如果同时有多个客户请求连接,这些客户就必须排队等候响应。

许多实际应用要求服务器具有同时为多个客户提供服务的能力。HTTP服务器就是最明显的例子。任何时刻,HTTP服务器都可能接收到大量的客户请求,每个客户都希望能快速得到HTTP服务器的响应。如果长时间让客户等待,会使网站失去信誉,从而降低访问量。因此可以使用多线程的方法使得服务器能够同时接受多个用户请求。

那么需要修改服务器端的代码,当accept()函数不再阻塞时,说明有客户端连入了服务器,那么新建一个线程,并把建立的socket传入线程中进行操作即可,代码如下:

public class ServerDemo {
    private int port = 40000;
    private ServerSocket serverSocket;
    
    
    
    class ThreadServer extends Thread {
        Socket socket;

        public PrintWriter getWriter(Socket socket) throws IOException {
            OutputStream socketoutput = socket.getOutputStream();
            PrintWriter printWriter = new PrintWriter(socketoutput, true);
            return printWriter;
        }

        public BufferedReader getReader(Socket socket) throws IOException {
            InputStream socketinput = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socketinput));

            return bufferedReader;
        }

        public ThreadServer(Socket s) {
            this.socket = s;
        }

        @Override
        public void run() {
            System.out.println("已有客户端连接,地址:" + socket.getInetAddress() + " 端口号:" + socket.getPort());

            try {
                PrintWriter writer = this.getWriter(socket);
                BufferedReader reader = this.getReader(socket);
                String msg = null;
                msg = reader.readLine();
                while ((msg) != null) {
                    System.out.println(socket.getInetAddress() + " " + socket.getPort() + " 发来的消息:" + msg);
                    writer.println("server收到了: " + msg);
                    msg = reader.readLine();
                }
            } catch (IOException e) {

            } finally {
                try {
                    if (socket != null)
                        socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
    
    
    
    
    public ServerDemo() throws IOException {
        serverSocket = new ServerSocket(port);
        System.out.println("服务端已启动");
    }
    public void server()
    {
        while(true)
        {
            Socket socket = null;
            try {
                socket = serverSocket.accept();//等待客户连接

                ThreadServer threadServer = new ThreadServer(socket);
                threadServer.start();
            } catch (IOException e) {
                System.out.println("客户端 :"+socket.getInetAddress()+" 断开连接, 服务器继续运行");
            }finally {

            }
        }
    }


    public static void main(String[] args) throws IOException {
        new ServerDemo().server();
    }
}
复制代码

客户端的代码不需要任何改动,在这里有一个需要注意的大坑:当设定成多线程模式时,建立的socket关闭代码就不能继续写在主线程的finally块下,否则在新建了一个线程后,主线程继续执行代码,会执行到finally将socket关闭,就会出现问题了!!!这样的话就可以多个客户端同时访问服务器了,效果如下:

在这里插入图片描述

关闭服务器Socket

ServerSocket 的 close() 方法使服务器释放占用的端口,并且断开与所有客户的连接。当一个服务器程序运行结束时,即使没有执行 ServerSocket 的 close() 方法,操作系统也会释放这个服务器占用的端口。因此,服务器程序并不一定要在结束之前执行 ServerSocket 的 close() 方法。

在某些情况下,如果希望及时释放服务器的端口,以便让其他程序能占用该端口,则可以显式调用ServerSocket 的 close() 方法。

获取ServerSocket信息

下面两个函数是 ServerSocket 提供的用来获取服务器绑定的IP地址以及绑定的端口号的函数:

public InetAddress getInetAddress()

public int getLocalPort()
复制代码

多数服务器会监听固定的端口,这样才便于客户程序访问服务器。不过匿名端口一般适用于服务器与客户之间的临时通信,通信结束,就断开连接,并且 ServerSocket 占用的临时端口也被释放。