Java Socket编程 一个BIO Socket客户端的进化

最近看了Java的IO包源码,对BIO有了较深刻的理解。Socket编程其实也是基于IO流操做,而且其流操做都是阻塞的,就想着写一个Socket程序并对其一步一步优化,来加深对IO的理解。本文主要从简单的Socket链接开始,一步一步优化,最后使用线程池等技术提升并发。Socket源码本篇未涉及,等有时间我再研究一番。java

一. 基本概念

Socket编程的基本流程以下图(图片来自网络),一个IP地址和一个端口号称为一个套接字(socket)。 spring

Socket
Socket编程是BIO的,对于服务端,accept()、read()、write()都会堵塞。

  • accept是阻塞的,只有新链接来了,accept才会返回,主线程才能继续
  • read是阻塞的,只有请求消息来了,read才能返回,子线程才能继续处理
  • write是阻塞的,只有客户端把消息收了,write才能返回,子线程才能继续读取下一个请求 Socket开发Java提供了两个类,Socket用于BIO链接和信息收发,ServerSocket用于构建一个服务端,其accept()方法得到一个Socket对象,最终客户端服务器都是使用Socket进行通讯。

二. 最基本的Socket

以下,最基本的客户端发送消息,服务端接收消息输入。须要注意的是,因为中文的utf8编码是3个字节,若是使用buffer来分段接收字节流,可能致使乱码。另外,read()是堵塞的,若是不判断read() == -1来表示结束,那么read()方法会一直堵塞。编程

package me.zebin.demo.javaio;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

@RunWith(SpringRunner.class)
@SpringBootTest
public class JavaioApplicationTests {

    @Test
    public void server() throws Exception {

        // 指定端口
        ServerSocket ss = new ServerSocket(9999);
        System.out.println("server starting...");
        // 等待链接
        Socket s = ss.accept();

        // 获取输入流,接收客户端的消息
        InputStream is = s.getInputStream();

        // 缓存buffer,utf8编码中文是3个字节,这里也但是使用BufferedReader解码
        byte[] buffer = new byte[5];
        while(true){
            int cnt = is.read(buffer);
            // 若是不判断流结束,上面的read()读不到数据会一直堵塞
            if(cnt == -1){
                break;
            }
            String str = new String(buffer, 0, cnt, "utf8");
            System.out.println(str);

        }
        s.close();
        ss.close();

    }

    @Test
    public void client() throws Exception{

        // 指定端口
        Socket s = new Socket("127.0.0.1", 9999);

        // 获取输出流,向服务端发消息
        OutputStream os = s.getOutputStream();

        // 发送消息,utf8编码中文是3个字节,服务端使用buffer可能致使乱码
        String str = "我是客户端";
        os.write(str.getBytes("utf8"));
        s.close();
    }
}
复制代码

以上程序,若是buffer设置为5,运行结果以下,出现乱码。 数组

乱码
固然,解决方案能够整行读取,将InputStream转为Reader再转BufferedReader便可读取一行。也可以使用Scanner来解决,服务端代码改成以下:

@Test
    public void server() throws Exception {

        // 指定端口
        ServerSocket ss = new ServerSocket(9999);
        System.out.println("server starting...");
        // 等待链接
        Socket s = ss.accept();

        // 获取输入流,接收客户端的消息
        InputStream is = s.getInputStream();

        // 输入字节流封装为Scanner,读取整行
        Scanner sc = new Scanner(is, "utf8");
        while (sc.hasNextLine()){
            System.out.println(sc.nextLine());
        }

        s.close();
        ss.close();

    }
复制代码

运行结果以下,没有乱码了。 缓存

Scanner
服务端判断流关闭,通常使用两种方法。

  1. 使用特殊符号:既然上面能够获取到行,服务端客户端就能够约定相关的结束符,如接收到一个空行就结束,服务端进行判断关闭流便可。
  2. 使用长度界定:相似http协议就有content-length界定结束符,咱们也能够在客户端发送byte[]数组前,在byte[]数据前两个字节标识消息长度。固然,两个字节能表示的消息长度就只有2^16-1,即大小是2^16字节,即64k大小。

三. 多线程版本

上面的版本有一个弊端,就是一个服务器只能提供给一个客户端进行链接,若是将链接的用线程处理,服务器能够处理更多的客户端链接,代码以下:服务器

@Test
    public void server() throws Exception {

        // 指定端口
        ServerSocket ss = new ServerSocket(9998);
        System.out.println("server starting...");
        while(true){
            // 等待链接
            Socket s = ss.accept();
            System.out.println("得到链接");
            Thread t = new Thread(new ServerThread(s));
            t.start();
        }
    }

    class ServerThread implements Runnable{

        private Socket s;

        ServerThread(Socket s){
            this.s = s;
        }

        @Override
        public void run(){
            // 获取输入流,接收客户端的消息
            InputStream is = null;
            try {
                is = s.getInputStream();
                // 使用Scanner封装
                Scanner sc = new Scanner(is, "utf8");
                while (sc.hasNextLine()){
                    System.out.println(sc.nextLine());
                }
                s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
复制代码

四. 线程池版本

以上多线程版本咱们使用了多线程来处理并发,不过线程的建立和销毁都会消耗大量的资源和时间,同时,高并发下会建立很是多的线程,且不说操做系统能开启的线程数有限,操做系统维护和切换大量的线程也会很是耗时。因此使用线程池,只用4个线程,用队列将未执行到的线程排队处理,减小了线程数量,同时也避免了建立和销毁线程带来的性能问题。网络

@Test
    public void server() throws Exception {

        // 指定端口
        ServerSocket ss = new ServerSocket(9998);
        System.out.println("server starting...");

        // 建立线程队列
        BlockingQueue bq = new ArrayBlockingQueue(100);
        // 拒绝策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
        Executor executor = new ThreadPoolExecutor(4, 8, 1, TimeUnit.MINUTES, bq, handler);
        while(true){
            // 等待链接
            Socket s = ss.accept();
            System.out.println("得到链接");
            Thread t = new Thread(new ServerThread(s));
            executor.execute(t);
        }
    }
复制代码

以上,本篇结束。多线程

参考资料

相关文章
相关标签/搜索