基于Java的Socket编程的简单学习

什么是Socket

网络由下往上分为 物理层 、数据链路层 、 网络层 、 传输层 、 会话层 、 表现层 和 应用层。IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP协议是应用层协议,主要解决如何包装数据。

socket,又称套接字,是在不同的进程间进行网络通讯的一种协议、约定或者说是规范。对于socket编程,它更多的时候像是基于TCP/UDP等协议做的一层封装或者说抽象,是一套系统所提供的用于进行网络通信相关编程的接口。

Socket的建立过程

基于Java的Socket编程的简单学习_第1张图片
本质上,socket是对tcp连接(当然也有可能是udp等其他连接)协议,在编程层面上的简化和抽象。

我们可以从最简单的单次通信做一个小demo进行学习

BaseSocket:

public class BaseSocket {

    public int port;
    public String host;
    public static final int MAX_BUFFER_SIZE = 1024; 
    public ServerSocket serverSocket;
    public Socket socket;
    public InputStream inputStream;
    public OutputStream outputStream;

    public void close() {

        try {
            if (this.inputStream != null) {
                this.inputStream.close();
            }
            if (this.outputStream != null) {
                this.outputStream.close();
            }
            if (this.socket != null) {
                this.socket.close();
            }
            if (this.serverSocket != null) {
                this.serverSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

BaseSocketServer:

public class BaseSocketServer extends BaseSocket {


    public int getPort() {
        return this.port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    BaseSocketServer(int port) {
        this.port = port;
    }

    /**
     * 单次通信
     */
    public void runServerSingle() {
        try {
            this.serverSocket = new ServerSocket(this.port);
            System.out.println("----------base socket server started------------");
            this.socket = serverSocket.accept();
            this.inputStream = socket.getInputStream();
            byte[] readBytes = new byte[MAX_BUFFER_SIZE];

            int msgLen;
            StringBuilder stringBuilder = new StringBuilder();

            while ((msgLen = inputStream.read(readBytes)) != -1) {
                stringBuilder.append(new String(readBytes, 0, msgLen, "UTF-8"));
            }

            System.out.println("Get message from client : " + stringBuilder.toString());

            this.close();

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

    /**
     * 双向通信
     */
    public void runServer() {
        try {
            this.serverSocket = new ServerSocket(port);
            System.out.println("----------base socket server started------------");
            this.socket = serverSocket.accept();
            this.inputStream = socket.getInputStream();
            byte[] readBytes = new byte[MAX_BUFFER_SIZE];

            int msgLen;
            StringBuilder stringBuilder = new StringBuilder();

            while ((msgLen = this.inputStream.read(readBytes)) != -1) {
                stringBuilder.append(new String(readBytes, 0, msgLen, "UTF-8"));
            }

            System.out.println("received message: " + stringBuilder.toString());

            //告诉客户端接受完毕,之后只能发送
            this.socket.shutdownInput();

            this.outputStream = socket.getOutputStream();

            String receipt = "we received your message : " + stringBuilder.toString();

            this.outputStream.write(receipt.getBytes("UTF-8"));

            this.close();

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


    public static void main(String[] args) {
        BaseSocketServer baseSocketServer = new BaseSocketServer(8888);
        //单向通信
        //baseSocketServer.runServerSingle();
        //双向通信
        baseSocketServer.runServer();
    }
}

BaseSocketClient:

public class BaseSocketClient extends BaseSocket {


    BaseSocketClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    /**
     * 获取连接
     */
    public void connectServer() {
        try {
            this.socket = new Socket(this.host, this.port);
            this.outputStream = this.socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 单向通信
     *
     * @param message 消息内容
     */
    public void sendSingle(String message) {
        try {
            this.outputStream.write(message.getBytes("UTF-8"));

            this.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
     /**
     * 双向通信
     * @param message 消息
     */
    public void sendMessage(String message) {
        try {

            this.outputStream.write(message.getBytes("UTF-8"));

            //发送完毕
            this.socket.shutdownOutput();

            this.inputStream = this.socket.getInputStream();
            byte[] readBytes = new byte[MAX_BUFFER_SIZE];
            int msgLen;
            StringBuilder stringBuilder = new StringBuilder();

            while ((msgLen = inputStream.read(readBytes)) != -1) {
                stringBuilder.append(new String(readBytes, 0, msgLen, "UTF-8"));
            }
            System.out.println("got receipt: " + stringBuilder.toString());

            this.inputStream.close();
            this.close();

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

    }

    public static void main(String[] args) {
        BaseSocketClient baseSocketClient = new BaseSocketClient("127.0.0.1", 8888);
        baseSocketClient.connectServer();
        //单向通信
        //baseSocketClient.sendSingle("hello");
        //双向通信
        baseSocketClient.sendMessage("hello");

    }
}
  • 先运行Server后运行Client便可以得到Client发送给Server的消息。这里的IO操作实现,我们使用了一个大小为MAX_BUFFER_SIZE的byte数组作为缓冲区,然后从输入流中取出字节放置到缓冲区,再从缓冲区中取出字节构建到字符串中去,这在输入流文件很大时非常有用。

单向通信详情可以参考,runServerSingle与sendSingle.
单向通信显然有点浪费通道。socket连接支持全双工的双向通信(底层是tcp),上边的例子中,双向通信,服务端在收到客户端的消息后,将返回给客户端一个回执。
双向通信与单向类似,不同的一点是,在 进行一次消息传递之后不是真正意义上的close资源。而是调用了

this.socket.shutdownOutput();
this.socket.shutdownInput();

借此告知服务端或者客户端消息已经发送\接受完毕。调用stream的close会导致sockt的关闭,虽然调用上面两个方法也会关闭流,但不会关闭socket,只是无法继续发送消息。

如何发送多条消息呢?

我们的上述例子在进行一次发送\回执之后就会close,没有办法进行接下来的消息发送。下次需要重新建立连接,这样是很耗费资源的。其实我们可以做到建立一次连接分次发送多条消息,我们有两张方式进行多条消息的区分。
我们先看一个例子:
CycleSocketServer:

public class CycleSocketServer extends BaseSocket{

    public int getPort() {
        return this.port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    CycleSocketServer(int port) {
        this.port = port;
    }

    public void runServerForSign() {
        try {
            this.serverSocket = new ServerSocket(this.port);
            System.out.println("base socket server started.");

            this.socket = serverSocket.accept();

            this.inputStream = socket.getInputStream();

            Scanner scanner = new Scanner(inputStream);

            //循环接收并打印消息
            while (scanner.hasNextLine()) {
                System.out.println("get info from client: " + scanner.nextLine());
            }
            scanner.close();
            this.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据长度界定,消息传递分为两步
     * 1.发送长度
     * 2.获取消息
     */
    public void runServer() {
        try {

            this.serverSocket = new ServerSocket(this.port);
            this.socket = serverSocket.accept();
            System.out.println("server socket running!");
            this.inputStream = socket.getInputStream();
            byte[] bytes;

            while (true) {
                //先读取第一个字节
                int first = this.inputStream.read();
                //是-1则表示输入流已经关闭
                if (first == -1) {
                    this.close();
                    break;
                }
                //读取第二个字节
                int second = this.inputStream.read();

                //用位运算将两个字节拼起来成为真正的长度
                int length = (first <<8 ) +second;

                bytes = new byte[length];

                this.inputStream.read(bytes);

                System.out.println("receive message : " + new String(bytes,"UTF-8"));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        CycleSocketServer cycleSocketServer = new CycleSocketServer(8888);

        //cycleSocketServer.runServerForSign();

        cycleSocketServer.runServer();

    }
}

CycleSocketClient:

public class CycleSocketClient extends BaseSocket {


    CycleSocketClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    /**
     * 获取连接
     */
    public void connectServer() {
        try {
            this.socket = new Socket(this.host, this.port);
            this.outputStream = this.socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendForSign(String message) {
        //约定 \n为消息结束标记
        String sendMsg = message + "\n";

        try {
            this.outputStream.write(sendMsg.getBytes("UTF-8"));


        } catch (IOException e) {
            System.out.println(sendMsg);
            e.printStackTrace();
        }
    }

    public void sendMessage(String message){

        try {
            //将message转化为bytes数组
            byte[] bytes = message.getBytes("UTF-8");

            //传输两个字节长度。采用位移实现
            int length = bytes.length;
            this.outputStream.write(length >> 8);
            this.outputStream.write(length);

            //传输完长度之后
            this.outputStream.write(bytes);

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

    }


    public static void main(String[] args) {
        CycleSocketClient cycleSocketClient = new CycleSocketClient("127.0.0.1", 8888);
        cycleSocketClient.connectServer();
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            String message = scanner.nextLine();

            //cycleSocketClient.send(message);
            cycleSocketClient.sendMessage(message);
        }
    }

}
  • 使用特殊符号作为结束限定
    最简单的办法是使用一些特殊的符号来标记一次发送完成,服务端只要读到对应的符号就可以完成一次读取,然后进行相关的处理操作。
    上面的例子中我们使用换行符\n来标记一次发送的结束,服务端每接收到一个消息,就打印一次,并且使用了Scanner来简化操作.
    运行后效果是,客户端每输入一行文字按下回车后,服务端就会打印出对应的消息读取记录。
    详情可以参考:runServerForSign与runServerForSign
  • 根据长度限定
    我们之所以不好定位消息什么时候结束,是因为我们不能够确定每次消息的长度。
    那么其实可以先将消息的长度发送出去,当服务端知道消息的长度后,就能够完成一次消息的接收了。
    总的来说,发送一次消息变成了两个步骤
  1. 发送消息的长度
  2. 发送消息
    最后的问题就是,“发送消息的长度”这一步骤所发送的字节量必须是固定的,否则我们仍然会陷入僵局。
    一般来说,我们可以使用固定的字节数来保存消息的长度,比如规定前2个字节就是消息的长度,不过这样我们能够传送的消息最大长度也就被固定死了,以2个字节为例,我们发送的消息最大长度不超过2^16个字节即64K。
    在上面的例子中,sendMessage与runServer 便是按照长度进行界定的。详情可以参考code

利用多线程实现server与client的交互

在上述的例子中,消息的接收方并不能主动地向对方发送消息,换句话说我们并没有实现真正的互相对话,这主要是因为消息的发送和接收这两个动作并不能同时进行,因此我们需要使用两个线程,其中一个用于监听键盘输入并将其写入socket,另一个则负责监听socket并将接受到的消息显示。出于简单考虑,我们直接让主线程负责键盘监听和消息发送,同时另外开启一个线程用于拉取消息并显示。
ListenThread:

public class ListenThread extends BaseSocket implements Runnable {
    ListenThread(Socket socket) {
        this.socket = socket;
        try {
            this.inputStream = socket.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                int first = this.inputStream.read();

                if (first == -1) {
                    this.close();
                    throw new RuntimeException("disconnected");
                    //break;
                }

                int second = this.inputStream.read();
                int msglen = (first << 8) + second;
                byte[] bytes = new byte[msglen];

                this.inputStream.read(bytes);

                System.out.println("message from  [ " + this.socket.getInetAddress() + " ] is " + new String(bytes, "UTF-8"));

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

        }
    }
}

ChatSocket:

public class ChatSocket extends BaseSocket {

    //ExecutorService threadPool = Executors.newFixedThreadPool(100);

    ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();

    /**
     * 使用factory
     * 创建线程池
     */
    ExecutorService threadPool = new ThreadPoolExecutor(5,10,1,
            TimeUnit.SECONDS,new ArrayBlockingQueue<>(1024),threadFactory,new ThreadPoolExecutor.AbortPolicy());

    public void runAsServer(int port) {
        try {
            this.serverSocket = new ServerSocket(port);

            System.out.println("server started at port " + port);

            //等待客户端的加入
            this.socket = this.serverSocket.accept();
            System.out.println("successful connected with " + socket.getInetAddress());

            //启动监听线程
             this.threadPool.submit(new ListenThread(this.socket));

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

    public void runAsClient(String host, int port) {
        try {
            this.socket = new Socket(host, port);
            System.out.println("successful connected to server " + socket.getInetAddress());
            this.threadPool.submit(new ListenThread(this.socket));
            waitAndSend();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void waitAndSend() {
        try {
            this.outputStream = this.socket.getOutputStream();
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()) {
                sendMessage(scanner.nextLine());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public void sendMessage(String message) {
        try {
            byte[] bytes = message.getBytes("UTF-8");
            int length = bytes.length;
            this.outputStream.write(length >> 8);
            this.outputStream.write(length);
            this.outputStream.write(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        ChatSocket chatSocket = new ChatSocket();
        System.out.println("select connect type: 1 for server and 2 for client");
        int type = scanner.nextInt();

        if (type == 1) {
            System.out.print("input server port :");
            int port = scanner.nextInt();
            chatSocket.runAsServer(port);
        } else if (type == 2) {
            System.out.print("input server host: ");
            String host = scanner.next();
            System.out.print("input server port: ");
            int port = scanner.nextInt();
            chatSocket.runAsClient(host, port);
        }
    }
}

作为服务端,如果一次只跟一个客户端建立socket连接,未免显得太过浪费资源,因此我们完全可以让服务端和多个客户端建立多个socket。

那么既然要处理多个连接,就不得不面对并发问题了(当然,你也可以写循环轮流处理)。我们可以使用多线程来处理并发,不过线程的创建和销毁都会消耗大量的资源和时间,所以最好一步到位,我们用一个线程池来实现。线程池的相关用法感兴趣的同学可以了解以下。

demo链接

你可能感兴趣的:(个人随笔)