手写ServerSocket实现与http通信

目录

    • 前言
    • 实现
    • http协议规范

前言

今天突然想到手写一个可以和http通信的简易ServerSocket,但是在完成这个功能时遇到了一些困难,故在此做个分享

实现

直接上代码吧,第一个版本代码如下

public class MyServlet2 {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8081);

        while(true) {
            Socket clientSocket = serverSocket.accept();

            InputStream inputStream = clientSocket.getInputStream();
            byte[] bytes = new byte[1024];
            int length;
            String str = "";
            // 核心代码
            while((length = inputStream.read(bytes)) > 0) {
                str += new String(bytes, 0, length, "UTF-8");
            }
            System.out.println(str);
        }

    }

}

这个版本一开始看上去好像没什么问题,但是当我通过浏览器输入http://localhost:8081/123时发现线程被阻塞了,然后最终定位到线程阻塞在while((length = inputStream.read(bytes)) > 0) 这一行,然后我查看以前读取文件流的方式,看了很久也没发现异常,没办法了只能去查阅相关资料

最终定位到的问题是,http和Socket通信时,服务端不知道客户端什么时候发送数据结束,所以执行inputStream.read时会一直等待,所以会阻塞

然后查询相关资料可解决方案有以下几种
1、设置客户端超时时间,超过指定时间则break循环,但是这种方式肯定是不靠谱的,总不能不让客户端长时间传输数据吧
2、根据http协议自己判断客户端什么时候发送数据

http协议规范

这里先普及下http协议的几个规范
1、http协议http头的结束必定是以结束符号\r\n\r\n结束(即两个回车符号)
2、如果请求包含请求体则http会存在字段Content-Length,如Content-Length :20(注意中间还有个空格),20表示请求体的字节长度
3、http请求头是和请求体连在一起传输的

ps(据说还有请求体内容不确定的,本文暂不考虑这种情况吧)

根据以上规范,2.0版本的代码如下

/**
 * @description 实现一个简易的http和servlet通信功能
 * http协议规则:
 * http头传输结束符号\r\n\r\n(即两个回车符号)
 * 有消息体则存在Content-Length字段,长度为body的字节数量
 *
 */
public class MyServlet {

    public static void main(String[] args) throws IOException {

        ServerSocket serverSocket = new ServerSocket(8081);
        while(true) {
            Socket clientSocket = serverSocket.accept();

            InputStream inputStream = clientSocket.getInputStream();
            // 缓冲区设置比较小,便于测试
            byte[] bytes = new byte[10];
            int length;

            byte[] allBytesArray = new byte[]{};
            // 核心代码,需要判断什么时候客户端请求发送结束
            while((length = inputStream.read(bytes)) > 0) {
                // 数组合并,使用字节累加,直接转String的话中文会被分割,导致乱码
                byte[] joinedArray = new byte[allBytesArray.length + length];
                System.arraycopy(allBytesArray, 0, joinedArray, 0, allBytesArray.length);
                System.arraycopy(bytes, 0, joinedArray, allBytesArray.length, length);
                allBytesArray = joinedArray;

                String str = new String(allBytesArray, "UTF-8");
                // 是否有消息体
                boolean existBody = str.contains("Content-Length");

                if(!existBody && str.lastIndexOf("\r\n\r\n") != -1) {
                    // 没有body体,则根据http协议,http header头结尾为\r\n\r\n,结尾是\r\n\r\n时判断数据接收完成
                    break;
                }
                
                // 有请求体并且http已经接收完成
                if(existBody && str.contains("\r\n\r\n")) {
                    System.out.println("-------------------存在body");
                    // 先等http头传输完成,截取头内容
                    String header = str.substring(0,str.indexOf("\r\n\r\n"));
                    System.out.println("-------------------header:" + header);

                    // 读取Content-Length的长度
                    int startIndex = header.indexOf("Content-Length");
                    int start = header.indexOf(" ",startIndex);
                    int end = header.indexOf("\r\n",startIndex);
                    // 截取请求体长度
                    int contentLength = Integer.valueOf(header.substring(start+1,end));
                    System.out.println("-------------------contentLength:" + contentLength);
                    int total = str.getBytes().length;
                    // 当前字节长度
                    int currentBodyLength = total - header.getBytes().length - "\r\n\r\n".getBytes().length;
                    System.out.println("-------------------currentBodyLength:" + currentBodyLength);
                    if(currentBodyLength == contentLength) {
                        //当前长度达到Content-Length表示接收完成
                        break;
                    }
                }
            }

            OutputStream outputStream = clientSocket.getOutputStream();
            outputStream.
                    write(("HTTP/1.1 200 OK\r\n" +  //响应头第一行
                            "Content-Type: text/html; charset=utf-8\r\n" +  //简单放一个头部信息
                            "\r\n" +  //这个空行是来分隔请求头与请求体的
                            "

hello world

\r\n"
).getBytes()); outputStream.close(); inputStream.close(); clientSocket.close(); } } }

经过测试,传递参数和不传递参数均可以正常使用。

本次2.0版本是基于BIO,并且只有单个线程处理,关闭流等方式也都是直接往外抛异常的,还可以使用线程池升级处理请求方式,或者使用Nio来实现会更好

PS(不知道Tomcat接收http请求是怎么判断的)

你可能感兴趣的:(手写ServerSocket实现与http通信)