初探Java Socket

前言

本篇文章将涉及如下内容:html

  • IO实现Java Socket通讯
  • NIO实现Java Socket通讯

阅读本文以前最好了解过:java

  • Java IO
  • Java NIO
  • Java Concurrency
  • TCP/IP协议

TCP 套接字

TCP套接字是指IP号+端口号来识别一个应用程序,从而实现端到端的通信。其实一个套接字也能够被多个应用程序使用,可是一般来讲承载的是一个应用程序的流量。创建在TCP链接之上最著名的协议为HTTP,咱们平常生活中使用的浏览器访问网页一般都是使用HTTP协议来实现的。面试

先来了解一下经过TCP套接字实现客户端和服务器端的通讯。编程

在TCP客户端发出请求以前,服务器会建立新的套接字(socket),并将套接字绑定到某个端口上去(bind),默认状况下HTTP服务的端口号为80。绑定完成后容许套接字进行链接(listen)并等待链接(accept)。这里的accept方法会挂起当前的进程直到有Socket链接。浏览器

在服务器准备就绪后,客户端就能够发起Socket链接。客户端获取服务器的Socket套接字(IP号:端口号),并新建一个本地的套接字。而后连同本地的套接字发送到服务器上。缓存

服务器accept该请求并读取该请求。这里面包括有TCP的三次链接过程。链接创建以后,客户端发送HTTP请求并等待响应。服务端根据HTTP报文返回响应,并关闭链接。服务器

Web Server

当下的Web服务器可以同时支持数千条链接,一个客户端可能向服务器打开一条或多条链接,这些链接的使用状态各不相同,使用率也差别很大。如何有效的利用服务器资源提供低延时的服务成了每一个服务器都须要考虑的问题。根据服务器的处理方式,能够分为如下4种服务器,咱们也将分别对其进行简单的实现。微信

  • 单线程服务器
  • 多进程及多线程服务器
  • 复用IO服务器
  • 复用的多线程服务器

单线程服务器

一次只处理一个请求,直到其完成为止。一个事务处理结束后,才会去处理下一条链接。实现简单,可是性能堪忧。多线程

多进程及多线程服务器

能够根据须要建立,或预先建立一下线程/进程。能够为每条链接分配一个线程/进程。可是当强求数量过多时,过多的线程会致使内存和系统资源的浪费。并发

复用I/O服务器

在复用结构中,会同时监视全部链接上的活动,当链接状态发生变化时,就对那条链接进行少许的处理。处理结束后,就将链接返回到开放链接列表中,等待下一次状态的变化。以后在有事情可作时才会对链接进行处理。在空闲链接上等待的时候不会绑定线程和进程。

复用的多线程服务器

多个线程(对应多个CPU)中的每个都在观察打开的链接(或是打开链接中的一个子集)。并对每条链接的状态变化时执行任务。

Socket通讯基本实现

根据咱们上面讲述的Socket通讯的步骤,在Java中咱们能够按照如下方式逐步创建链接:

首先开启服务器端的SocketServer而且将其绑定到一个端口等待Socket链接:

ServerSocket serverSocket = new ServerSocket(PORT_ID:int);
Socket socket = serverSocket.accept();

当没有Socket链接时,服务器会在accept方法处阻塞。

而后咱们在客户端新建一个Socket套接字而且链接服务器:

Socket socket = new Socket(SERVER_SOCKET_IP, SERVER_SOCKET_PORT);
socket.setSoTimeout(100000);

若是链接失败的话,将会抛出异常说明服务器当前不可使用。
链接成功给的话,客户端就能够获取Socket的输入流和输出流并发送消息。写入Socket的输出流的信息将会先存储在客户端本地的缓存队列中,知足必定条件后会flush到服务器的输入流。服务器获取输入后能够解析输入的数据,而且将响应内容写入服务器的输出流并返回客户端。最后客户端从输入流读取数据。

客户端获取Socket输入输出流,这里将字节流封装为字符流。

//获取Socket的输出流,用来发送数据到服务端
PrintStream out = new PrintStream(socket.getOutputStream());
//获取Socket的输入流,用来接收从服务端发送过来的数据
BufferedReader buf =  new BufferedReader(new InputStreamReader(socket.getInputStream()));

客户端发送数据并等待响应

String str = "hello world";
out.println(str);
String echo = buf.readLine();
System.out.println("收到消息:" + echo);

这里须要注意的是,IO流是阻塞式IO,所以在读取服务端响应的过程当中(即buf.reaLine()这一行)会阻塞直到收到服务器响应。

客户端发送结束以后不要忘了关闭IO和Socket通讯

out.close();
buf.close();
socket.close();

服务器对消息的处理和客户端相似,后面会贴上完整代码。

Java Socket通讯阻塞式通讯实现

这里咱们对上述的理论进行简单的实现。这里咱们实现一个简单的聊天室,只不过其中一方是Server角色而另外一个为Client角色。两者都经过System.in流输入数据,并发送给对方。正如咱们前面所说,IO流的通讯是阻塞式的,所以在等待对方响应的过程当中,进程将会挂起,咱们这时候输入的数据将要等到下一轮会话中才能被读取。

client端

import java.io.*;
import java.net.Socket;
import java.net.SocketTimeoutException;

public class SocketClient {

    public static void send(String server, int port){
        try {
            Socket socket = new Socket(server, port);
            socket.setSoTimeout(100000);

            System.out.println("正在链接服务器");

            //从控制台读入数据
            BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

            //获取Socket的输出流,用来发送数据到服务端
            PrintStream out = new PrintStream(socket.getOutputStream());
            //获取Socket的输入流,用来接收从服务端发送过来的数据
            BufferedReader buf =  new BufferedReader(new InputStreamReader(socket.getInputStream()));
            boolean running = true;
            while(running){
                System.out.print("输入信息:");
                String str = input.readLine();
                out.println(str);

                if("bye".equals(str)){
                    running = false;
                }else{
                    try{
                        //从服务器端接收数据有个时间限制(系统自设,也能够本身设置),超过了这个时间,便会抛出该异常
                        String echo = buf.readLine();
                        System.out.println("收到消息:" + echo);
                    }catch(SocketTimeoutException e){
                        System.out.println("Time out, No response");
                    }
                }
            }

            input.close();
            socket.close();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        }
    }

    public static void main(String[] args){
        send("127.0.0.1", 2048);
    }
}

Server端

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {

    public static void main(String[] args) throws IOException {
        //服务端在2048端口监听客户端请求的TCP链接
        ServerSocket server = new ServerSocket(2048);
        Socket client = null;
        boolean f = true;
        while(f){
            //等待客户端的链接,若是没有获取链接
            client = server.accept();
            System.out.println("与客户端链接成功!");
            //为每一个客户端链接开启一个线程
            new Thread(new ServerThread(client)).start();

        }
        server.close();
    }

}

服务器处理数据

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;

public class ServerThread implements Runnable{
    private Socket client = null;
    public ServerThread(Socket client){
        this.client = client;
    }

    @Override
    public void run() {
        try{
            //获取Socket的输出流,用来向客户端发送数据
            PrintStream out = new PrintStream(client.getOutputStream());

            //获取Socket的输入流,用来接收从客户端发送过来的数据
            BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));

            BufferedReader serverResponse = new BufferedReader(new InputStreamReader(System.in));
            boolean flag =true;
            while(flag){
                //接收从客户端发送过来的数据
                String str =  buf.readLine();
                System.out.println("收到消息:" + str);
                if(str == null || "".equals(str)){
                    flag = false;
                }else{
                    if("bye".equals(str)){
                        flag = false;
                    }else{
                        //将接收到的字符串前面加上echo,发送到对应的客户端
                        System.out.print("发送回复:");
                        String response  = serverResponse.readLine();
                        out.println(response);
                    }
                }
            }
            out.close();
            client.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

能够和小伙伴试试看,分别启动SocketServerSocketClient并进行通讯。不过前提是大家两个须要在一个局域网中。

Java实现单线程服务器

上面的服务器其实只在主线程监听了一个Socket链接,并在30秒以后将其自动关闭了。咱们将实现一个经典的单线程服务器。原理和上面类似,这里咱们能够直接经过向服务器发送HTTP请求来验证该服务器的运行。

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class SingleThreadServer implements Runnable{

    private ServerSocket serverSocket;

    public SingleThreadServer(ServerSocket serverSocket){
        this.serverSocket = serverSocket;
    }
    @Override
    public void run() {
        Socket socket = null;
        try{
            while (!Thread.interrupted()){
                socket = serverSocket.accept();

                //谷歌浏览器每次会发送两个请求
                //一次用于获取html
                //一次用于获取favicon
                //若是获取favicon成功就缓存,不然会一直请求得到favicon
                //而火狐浏览器第一次也会发出这两个请求
                //在得到favicon失败后就不会继续尝试获取favicon
                //所以使用谷歌浏览器访问该Server的话,你会看到 链接成功 被打印两次
                System.out.println("链接成功");
                process(socket);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void process(Socket socket){
        try {

            InputStreamReader inputStreamReader = null;
            BufferedOutputStream bufferedOutputStream = null;
            try{
                inputStreamReader = new InputStreamReader(socket.getInputStream());
                bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());

                //这里没法正常读取输入流,由于在没有遇到EOF以前,流会任务socket输入还没有结束,将会继续等待直到socket中断
                //因此这里咱们将暂时不读取Socket的输入流中的内容。
                //int size;
                //char[] buffer = new char[1024];
                //StringBuilder stringBuilder = new StringBuilder();
                //while ((size = inputStreamReader.read(buffer)) > 0){
                  //  stringBuilder.append(buffer, 0, size);
                //}


                byte[] responseDocument = "<html><body> Hello World </body></html>".getBytes("UTF-8");
                byte[] responseHeader = ("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: " + responseDocument.length + "\r\n\r\n").getBytes("UTF-8");

                bufferedOutputStream.write(responseHeader);
                bufferedOutputStream.write(responseDocument);
            }finally {
                bufferedOutputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

该服务器用单一线程处理每次请求,每一个线程都将等待服务器处理完上一个请求以后才能得到响应。这里须要注意,纯HTTP请求的输入流的读取会遇到输入流阻塞的问题,由于HTTP请求并无输入流可识别的EOF标记。从而致使服务器一直挂起在读取输入流的地方。它的解决方法以下:

  • 客户端关闭Socket链接,强制服务器关闭该Socket链接。可是同时也丢失服务器响应
  • 自定义协议,从而服务器能够识别数据的终点。

启动服务器

public static void main(String[] args) throws IOException, InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        ServerSocket serverSocket = new ServerSocket(2048);
        executorService.execute(new SingleThreadServer(serverSocket));

//        TimeUnit.SECONDS.sleep(10);
//        System.out.println("shut down server");
//        executorService.shutdownNow();
    }

注意要先关闭以前占用2048端口号的服务器。

咱们也可使用代码来测试:

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

public class TestSingleThreadServer {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0 ; i<10 ; i++){
            final int threadId = i;
            executorService.execute(() ->{

                try {
                    Socket socket = new Socket("127.0.0.1", 20006);
                    socket.setSoTimeout(5000);

                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String line = bufferedReader.readLine();

                    System.out.println(threadId + ":" + line);

                    socket.close();;
                } catch (IOException e) {
                    e.printStackTrace();
                }

            });

        }

        TimeUnit.SECONDS.sleep(40);
        executorService.shutdownNow();
    }
}

Java实现多线程服务器

这里咱们将为每个Socket链接提供一个线程来处理。基本实现和上面差很少,只是将每个Socket链接丢给一个额外的线程来处理。这里能够参考前面的简易聊天室来试着本身实现如下。

Java NIO实现复用服务器

NIO的出现改变了旧式Java读取IO流的方式。首先,它支持非阻塞式读取,其次它可使用一个线程来管理多个信道。多线程表面上看起来能够同时处理多个Socket通讯,可是多线程的管理自己也消耗至关多的资源。其次,不少信道的使用率每每并不高,一些信道每每并非连通状态中。若是咱们能够将资源直接赋予当前活跃的Socket通讯的话,能够明显的提升资源利用率。

先附上参考资料将在后序更新。

参考书籍

HTTP权威指南
Java TCP/IP Socket 编程
Java Multithread servers
Java NIO ServerSocketChannel

clipboard.png
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注个人微信公众号!将会不按期的发放福利哦~

相关文章
相关标签/搜索