TCP、Socket、HTTP之间的关系及用Socket实现http服务器和客户端

36 阅读4分钟

TCP(Transmission Control Protocol)和Socket在某种程度上可以类比为交通系统中的公路和车辆。

就像公路是实现车辆之间运输的基础设施一样,TCP协议也是实现网络数据传输的基础设施。TCP负责在网络中建立数据传输的连接,并保证数据的可靠传输。Socket则类似于汽车、卡车等车辆,它们通过公路来相互通信、运输货物、人员等。

在计算机网络中,Socket是实现TCP协议的编程接口,它提供了一组用于建立TCP连接、发送和接收数据的API。程序员可以利用Socket API来创建一个TCP连接,并通过该连接发送和接收数据。

HTTP(HyperText Transfer Protocol)是万维网(World Wide Web)的基础协议。它在现有的 TCP/IP 协议基础之上建立。TCP可以看作是道路,Socket可以看作是车辆,而HTTP就相当于车辆运输的货物。

  • TCP - 道路
  • Socket - 车辆
  • HTTP - 货物

道路不只有TCP这一种,还有一个UDP,只不过TCP这条路上。跑的都是安全性很高的车辆,并且都是按顺序、按轨道整整齐齐的向前开,而UDP则是没有固定的轨道,大家都是加足马力往前开,跑的可能很快,但是顺序就无法保证了。

而Socket也不止一种,不同道路跑不同的车辆,UDP这条道上跑的是DatagramSocket这种车。

而货物种类就更多了,除了HTTP,FTP(File Transfer Protocol)、SMTP(Simple Mail Transfer Protocol)等都可以用Socket来装载。

了解了它们之间的关系,那我们能不能使用Socket实现一个HTTP服务器或HTTP客户端呢,当然可以,完整的HTTP协议当然很复杂,但是我们只是为了了解其原理,所以怎么简单怎么来。

先使用Socket实现一个HTTP服务器:

public class HttpServer {
​
    public static void main(String[] args) throws Exception {
        // 本机地址5555,启动之后,打开浏览器访问:http://localhost:5555
        ServerSocket serverSocket = new ServerSocket(5555);
        while (true) {
            Socket socket = serverSocket.accept();
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String line = null;
            while (!"".equals(line) && (line = reader.readLine()) != null) {
                // 这里会输出请求头
                // 比如下面这样
                // GET /hello HTTP/1.1
                // Host: localhost:5555
                // 其他请求头 ...
                System.out.println(line);
            }
            // 输出响应
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            String response =
                "HTTP/1.1 200 OK\n" +
                "Content-Type: text/html\n" +
                "Content-Length: 14\n" +
                "\n" +
                "<h1>hello</h1>";
            writer.write(response);
            writer.flush();
            writer.close();
            socket.close();
        }
    }
}

浏览器访问以下地址 http://localhost:5555,可以看到下面的页面:

image-20230516164710654.png

是不是很简单,知道了原理,剩下的就需要了解HTTP协议的标准,等完全实现了HTTP标准协议,一个HTTP服务器做就出来了。

再用Socket做一个HTTP客户端:

public class SocketUtil {
    private static final Logger logger = LoggerFactory.getLogger(SocketUtil.class);
​
    public static void main(String[] args) throws Exception {
        String request =
            "GET /hello HTTP/1.1\n" +
            "Host: 127.0.0.1:5555\n" +
            "\n";
        byte[] bytes = sendSocket("127.0.0.1", 5555, request.getBytes(), true);
        System.out.println("响应内容 = " + new String(bytes));
    }
​
    /**
     * Socket 请求方法
     *
     * @param host           socket 地址
     * @param port           socket 端口
     * @param data           要发送的数据
     * @param isReadResponse 是否读取响应结果,true读取后续响应内容,false不读取返回null
     * @return 返回接收到的数据 byte[]
     * @throws IOException
     */
    public static byte[] sendSocket(String host, int port, byte[] data, boolean isReadResponse) throws Exception {
        logger.info("准备连接socket,address={},port={}", host, port);
        // 使用try-with-resources语句,自动关闭资源
        try (Socket socket = new Socket(host, port);
             BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());
             BufferedInputStream bufferedInputStream = new BufferedInputStream(socket.getInputStream())) {
​
            logger.info("socket连接成功,准备发送数据...");
            bufferedOutputStream.write(data);
            bufferedOutputStream.flush();
            logger.info("socket发送数据成功");
            // 是否需要读取响应内容,true读取后续响应内容,false不读取直接返回
            if (!isReadResponse) {
                return null;
            }
​
            // 从bufferedInputStream读取响应内容,3秒内读取不到,超时退出
            int i = 1;
            while (bufferedInputStream.available() <= 0) {
                Thread.sleep(1000);
                if (i++ >= 3) {
                    logger.info("socket读取返回结果超时");
                    return null;
                }
            }
            int len;
            byte[] result = new byte[1024];
            List<byte[]> list = new ArrayList<>();
            logger.info("socket正在读取返回结果...");
            while ((len = bufferedInputStream.read(result)) != -1) {
                byte[] bytes = new byte[len];
                System.arraycopy(result, 0, bytes, 0, len);
                list.add(bytes);
                if (bufferedInputStream.available() <= 0) {
                    break;
                }
            }
            logger.info("socket已经读取完成返回结果");
            if (list.size() == 0) {
                return new byte[0];
            }
            // 组合所有的bytes为一个byte[]
            return list.stream().reduce((a, b) -> {
                byte[] bytes = new byte[a.length + b.length];
                System.arraycopy(a, 0, bytes, 0, a.length);
                System.arraycopy(b, 0, bytes, a.length, b.length);
                return bytes;
            }).get();
        }
    }
}

访问刚才启动的服务器,接收到响应:

image-20230516170543723.png

可以在这里看到HTTP 1.1协议标准 RFC 2068 - 超文本传输协议 -- HTTP/1.1 (ietf.org)

现在流行微服务架构,服务之间的通信方式有很多,可以直接用http,也可以用dubbo之类的rpc框架,现在了解了Socket的用法,你完全可以自己定义一套协议标准,然后实现这套协议,用于服务间的通信。