HTTP 协议及 Java 套接字模拟 HTTP 客户与服务器程序
HTTP 协议及 Java 套接字模拟 HTTP 客户与服务器程序
HTTP 协议(Hypertext Transfer Protocol,超文本传输协议),顾名思义,是关于如何在网络上传输超文本(即 HTML 文档)的协议。HTTP 协议规定了 Web 的基本运作过程,以及浏览器与 Web 服务器之间的通信细节。HTTP 协议采用客户/服务器(C/S)通信模式,是目前在 Internet 上应用最广泛的通信协议之一。
如图所示,在分层的网络体系结构中,HTTP 协议位于应用层,建立在 TCP/IP 协议的基础上。HTTP 协议使用可靠的 TCP 连接,默认端口是 80 端口

| 分层 | 功能描述 | 数据单位 | 常见组件/协议/技术 |
|---|---|---|---|
| 应用层 | 为操作系统或网络应用程序提供网络服务的接口 | - | Telnet、HTTP、DNS、HTTPS、SNMP |
| 表示层 | 提供数据格式转换服务(加密与解密、图片编码和解码、数据压缩和解压缩等) | - | URL 加密、口令加密、图片编码解码 |
| 会话层 | 建立端连接并提供访问验证和会话管理(session) | - | 服务器验证登录、断点续传 |
| 传输层 | 提供应用进程间的逻辑通信(建立连接、处理数据包错误、数据包次序等) | 数据段(segment) | TCP、UDP、SPX、进程、端口(socket) |
| 网络层 | 为数据在结点之间传输创造逻辑链路、并分组转发数据(子网间路由选择等) | 数据包(package) | 路由器、多层交换机、防火墙、IP、IPX、RIP、OSPF |
| 数据链路层 | 在通信的实体间建立数据链路连接(数据分帧、流控制、物理地址寻址、重发等) | 帧(frame) | 网卡、网桥、二层交换机 |
| 物理层 | 为数据端设备提供原始比特流的传输通路(网络通信的数据传输介质相关) | 比特(bit) | 中继机、集线器、网线、HUB、RJ-45 标准 |
HTTP 协议运作过程
HTTP 协议规定 Web 的基本运作过程基于客户/服务器通信模式,客户端主动发出 HTTP 请求,服务端接收 HTTP 请求,再返回相应的 HTTP 响应结果。客户端与服务器之间的一次信息交换包括以下过程:
- 客户端与服务器端建立 TCP 连接
- 客户端发出 HTTP 请求
- 服务器端发出相应的 HTTP 响应
- 客户端与服务器端之间的 TCP 连接关闭

HTTP 协议特点
基于请求-响应的模式
HTTP 协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有接收到请求之前不会发送响应。
浏览器功能
- 请求与 Web 服务器建立 TCP 连接
- 创建并发送 HTTP 请求
- 接收并解析 HTTP 响应
- 在窗口中展示 HTML 文档
Web 服务器功能
- 接收来自浏览器的 HTTP 请求
- 接收并解析 HTTP 请求
- 创建并发送 HTTP 响应
目前最常用的 HTTP 客户程序包括 IE、Chrome、Firefox、Opera 和 Netscape 等,最常用的 HTTP 服务器包括 IIS 和 Apache 等。
无状态保存
HTTP 是一种不保存状态,即无状态(stateless)协议。HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。使用 HTTP 协议,每当有新的请求发送时,就会有对应的新响应产生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。
无连接
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间,并且可以提高并发性能,不能和每个用户建立长久的连接,请求一次响应一次,服务端和客户端就中断了。
无连接有两种方式:
- 早期的 HTTP 协议:一个请求一个响应之后,直接断开连接
- HTTP 1.1 版本:请求响应后,保持连接几秒。若用户在这几秒内有新的请求,通过之前的连接通道收发消息;若超过几秒无新请求,则断开连接。此举可提高效率,减少短时间内建立连接的次数(建立连接耗时耗资源)
简单快速
客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS。每种方法规定了客户与服务器联系的类型不同。
由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。
其中 GET 与 POST 请求方式最常用,二者区别如下:
- GET 提交的数据会放在 URL 之后,以
?分割 URL 和传输数据,参数之间以&相连(如EditBook?name=test1&id=123456);POST 方法是把提交的数据放在 HTTP 包的请求体中 - GET 提交的数据大小有限制(浏览器对 URL 的长度有限制),而 POST 方法提交的数据没有限制
- GET 与 POST 请求在服务端获取请求数据方式不同
灵活
HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记【MIME】
HTTP 请求头与响应头
请求头
请求头包含许多有关客户端环境和请求正文的有用信息。例如:浏览器的类型、所用的语言、请求正文的类型,以及请求正文的长度等。
请求头和请求正文之间必须以空行分割。

响应头
由三部分构成:
- HTTP 协议的版本、状态码和描述
- 响应头
- 响应正文

状态码解释
- 1××:保留
- 2××:表示请求成功地接收
- 3××:为完成请求客户需进一步细化请求
- 4××:客户错误(404:请求内容不存在;405 等)
- 5××:服务器错误
用 Java 套接字创建 HTTP 客户与服务器程序
前提准备
- 目录结构

- 将
static.html内容放在resources/pages/目录下,内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>漂浮广告案例</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 200vh; /* 让页面有滚动条,展示漂浮效果 */
background-color: #f0f0f0;
}
/* 广告容器样式 */
#floatAd {
position: fixed; /* 固定定位,相对于视口 */
width: 150px;
height: 200px;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transition: transform 0.2s ease;
}
#floatAd img {
width: 100%;
height: 100%;
object-fit: cover;
}
#floatAd:hover {
transform: scale(1.05);
}
</style>
</head>
<body>
<!-- 漂浮广告元素 -->
<div id="floatAd">
<img src="../photo/image.png" alt="广告图片">
</div>
<script>
// 获取广告元素
const floatAd = document.getElementById('floatAd');
// 初始位置和速度
let posX = 100;
let posY = 100;
let speedX = 3;
let speedY = 2;
// 设置初始位置
floatAd.style.left = posX + 'px';
floatAd.style.top = posY + 'px';
// 动画计时器
let animationId;
// 漂浮动画函数
function floatAnimation() {
// 获取视口尺寸
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 获取广告尺寸
const adWidth = floatAd.offsetWidth;
const adHeight = floatAd.offsetHeight;
// 更新位置
posX += speedX;
posY += speedY;
// 检测左右边界碰撞
if (posX <= 0 || posX >= windowWidth - adWidth) {
speedX = -speedX; // 反向
// 确保不会超出边界
posX = Math.max(0, Math.min(posX, windowWidth - adWidth));
}
// 检测上下边界碰撞
if (posY <= 0 || posY >= windowHeight - adHeight) {
speedY = -speedY; // 反向
// 确保不会超出边界
posY = Math.max(0, Math.min(posY, windowHeight - adHeight));
}
// 应用新位置
floatAd.style.left = posX + 'px';
floatAd.style.top = posY + 'px';
// 继续动画
animationId = requestAnimationFrame(floatAnimation);
}
// 开始动画
animationId = requestAnimationFrame(floatAnimation);
// 鼠标悬停停止动画
floatAd.addEventListener('mouseenter', () => {
cancelAnimationFrame(animationId);
});
// 鼠标离开继续动画
floatAd.addEventListener('mouseleave', () => {
animationId = requestAnimationFrame(floatAnimation);
});
// 窗口大小改变时重新计算
window.addEventListener('resize', () => {
// 确保广告不会超出新的窗口边界
posX = Math.max(0, Math.min(posX, window.innerWidth - floatAd.offsetWidth));
posY = Math.max(0, Math.min(posY, window.innerHeight - floatAd.offsetHeight));
// 更新位置
floatAd.style.left = posX + 'px';
floatAd.style.top = posY + 'px';
});
</script>
</body>
</html>
- 在
photo目录下添加一张任意照片(命名为image.png)
Java 代码
package com.http;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executors;
/**
* Http服务器:(主要针对静态资源)
* 接收客户端请求
* 处理请求,给出响应
* @author Jing61
*/
public class HttpServer {
public static void main(String[] args) {
// 创建线程池处理请求,实际开发中不建议使用Executors
var executor = Executors.newVirtualThreadPerTaskExecutor();
// 创建一个ServerSocket,监听端口8848
try(var server = new ServerSocket(8848)) {
while(true) { // 一直接收等待请求
// 接收客户端请求
var socket = server.accept(); // 接收客户端请求,如果没有请求,则阻塞在这个位置
// 处理请求
executor.execute(new HttpHandler(socket));
}
} catch(IOException e) {
System.out.println(e.getMessage());
}
}
/**
* 处理客户端请求
* 1. 接收请求
* 2. 给请求进行响应(静态资源)
* 根据url定位静态资源(在资源resources文件中加入静态资源,例如/page/static.html)
* 根据资源类型进行响应
*/
public static class HttpHandler implements Runnable {
private final Socket socket;
public HttpHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try(var input = socket.getInputStream();
var output = socket.getOutputStream()) { // 获取输入输出流
/*
* available():在读写操作前得知数据流中有多少个字节可以读取
* 该方法用于网络数据读取时,容易出现错误:当使用available读取时,对方发送的数据可能还没有到达,得到的长度为0
*/
var size = input.available();
while(size == 0) size = input.available(); // 等待数据到达
// 读取请求
byte[] buffer = new byte[size];
input.read(buffer);
// 将字节数组转成字符串
var request = new String(buffer);
System.out.println(request);
// 请求路径
var path = request.split("\r\n")[0].split(" ")[1];
if(path.contains("?")) path = path.substring(0, path.indexOf("?"));
System.out.println("请求的url:" + path);
// 决定http响应类型
String contentType;
if(path.indexOf(".html") != -1 || path.indexOf(".htm") != -1)
contentType = "text/html; charset=UTF-8"; // html
else if(path.indexOf(".jpg") != -1 || path.indexOf(".jpeg") != -1)
contentType = "image/jpeg"; // jpg
else if(path.indexOf(".gif") != -1)
contentType = "image/gif"; // gif
else
contentType = "application/octet-stream"; // 其他文件
var file = new File("http/src/main/resources" + path);
if (file.exists()) {
// 文件存在,响应请求:200
// 响应头
output.write("HTTP/1.1 200 OK\r\n".getBytes());
// 响应正文和响应头之间有一个空行
output.write(("content-type: " + contentType + "\r\n\r\n").getBytes());
var in = new BufferedInputStream(new FileInputStream(file));
int len = -1;
var buff = new byte[1024 * 8]; // 一次读取8KB
while((len = in.read(buff)) != -1) output.write(buff, 0, len);
in.close();
} else {
// 文件不存在,响应请求:404
output.write("HTTP/1.1 404 NOT FOUND\r\n".getBytes());
output.write(("content-type: text/html; charset=UTF-8\r\n\r\n").getBytes());
output.write("404,你所请求的资源不存在".getBytes());
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 关闭socket资源
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
运行效果
-
运行程序后,浏览器输入
http://localhost:8848/pages/static.html,可看到漂浮广告页面
![image]()
-
输入不存在的资源(如
http://localhost:8848/pages/404.html),显示 404 提示
![image]()
-
响应图片资源,输入
http://localhost:8848/photo/image.png,可查看图片
![image]()
代码解释
该程序通过 Socket 套接字 实现了一个简易的 HTTP 服务器,核心功能是接收客户端(如浏览器)的 HTTP 请求,定位并返回静态资源(HTML、图片等),下面分模块详细解释代码逻辑:
整体结构
程序包含两个核心部分:
- 外部类
HttpServer:负责创建服务器套接字、监听端口、接收客户端连接,并通过线程池分发请求任务 - 内部类
HttpHandler:实现Runnable接口,负责具体的请求处理(解析请求、定位资源、生成响应)
HttpServer 类
package com.http;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executors;
public class HttpServer {
public static void main(String[] args) {
// 1. 创建线程池:采用虚拟线程池(Java 19+ 特性),每个任务对应一个虚拟线程,轻量高效
// 注意:实际开发中不建议直接使用 Executors,建议自定义线程池(如 ThreadPoolExecutor)以控制核心参数
var executor = Executors.newVirtualThreadPerTaskExecutor();
// 2. 创建 ServerSocket 并监听 8848 端口
// try-with-resources 语法:自动关闭 ServerSocket,避免资源泄漏
try(var server = new ServerSocket(8848)) {
// 3. 无限循环:一直接收客户端连接(服务器常驻逻辑)
while(true) {
// accept() 方法:阻塞等待客户端连接,连接成功后返回一个 Socket 对象(代表客户端与服务器的通信通道)
var socket = server.accept();
// 4. 提交请求处理任务到线程池:避免主线程阻塞,支持并发处理多个客户端请求
executor.execute(new HttpHandler(socket));
}
} catch(IOException e) {
// 捕获 ServerSocket 创建或监听时的异常(如端口被占用)
System.out.println("服务器启动失败:" + e.getMessage());
}
}
// ... 内部类 HttpHandler 省略
}
关键:
ServerSocket:服务器端套接字,用于监听指定端口的客户端连接请求accept():阻塞方法,直到有客户端连接才返回,返回的Socket是客户端与服务器的“通信桥梁”- 线程池:解决“一个连接一个线程”的资源浪费问题,虚拟线程池比传统线程池更轻量,支持更高并发
HttpHandler 类
该类是程序的核心,负责完成“接收请求 → 解析请求 → 定位资源 → 生成响应”的完整流程,代码分步骤解释如下:
构造方法与资源初始化
public static class HttpHandler implements Runnable {
private final Socket socket; // 与客户端通信的 Socket
// 构造方法:接收 ServerSocket.accept() 返回的 Socket 对象
public HttpHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// try-with-resources 语法:自动关闭输入流、输出流,避免资源泄漏
try(var input = socket.getInputStream(); // 获取客户端发送数据的输入流
var output = socket.getOutputStream()) { // 向客户端发送数据的输出流
// ... 核心处理逻辑
} catch (IOException e) {
throw new RuntimeException("请求处理失败:" + e.getMessage(), e);
} finally {
// 最终关闭 Socket:无论请求处理成功与否,都要释放连接资源
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException("Socket 关闭失败:" + e.getMessage(), e);
}
}
}
}
关键:
Socket.getInputStream():读取客户端发送的 HTTP 请求数据(如请求行、请求头)Socket.getOutputStream():向客户端写入 HTTP 响应数据(如响应行、响应头、响应体)finally块:确保Socket最终被关闭,避免连接泄漏
读取 HTTP 请求数据
// 读取请求数据:解决网络数据延迟问题(available() 可能先返回 0)
var size = input.available(); // 获取输入流中可读取的字节数
while(size == 0) size = input.available(); // 等待数据到达(简易处理,实际可优化为超时机制)
// 创建字节数组缓冲区,读取请求数据
byte[] buffer = new byte[size];
input.read(buffer); // 将输入流中的数据读入缓冲区
// 将字节数组转为字符串(HTTP 请求是文本协议,按默认编码解析)
var request = new String(buffer);
System.out.println("接收的 HTTP 请求:\n" + request);
关键:
available():返回输入流中当前可读取的字节数,但网络传输存在延迟,可能先返回 0,因此需要循环等待- 局限性:该方法仅适用于简易场景,实际开发中建议使用
BufferedReader按行读取,避免因数据未完全到达导致的读取不完整问题
解析请求路径(核心步骤)
HTTP 请求的第一行(请求行)格式为:请求方法 请求路径 协议版本(如 GET /pages/static.html HTTP/1.1),代码通过字符串分割提取请求路径:
// 1. 按 "\r\n" 分割请求内容,取第一行(请求行)
// 2. 按 " " 分割请求行,取第二个元素(请求路径,如 /pages/static.html)
var path = request.split("\r\n")[0].split(" ")[1];
// 处理带参数的路径(如 /pages/static.html?id=1):截取 "?" 之前的部分
if(path.contains("?")) path = path.substring(0, path.indexOf("?"));
System.out.println("解析后的请求路径:" + path);
示例:
- 原始请求行:
GET /pages/static.html?id=1 HTTP/1.1 - 分割后得到的
path:/pages/static.html(去除参数部分)
确定响应内容类型(MIME 类型)
HTTP 响应需要通过 Content-Type 告诉客户端响应体的类型(如 HTML、图片),代码根据请求路径的后缀判断类型:
String contentType; // 响应的 Content-Type 字段值
if(path.indexOf(".html") != -1 || path.indexOf(".htm") != -1) {
contentType = "text/html; charset=UTF-8"; // HTML 文件:指定 UTF-8 编码避免乱码
} else if(path.indexOf(".jpg") != -1 || path.indexOf(".jpeg") != -1) {
contentType = "image/jpeg"; // JPG 图片
} else if(path.indexOf(".gif") != -1) {
contentType = "image/gif"; // GIF 图片
} else {
contentType = "application/octet-stream"; // 其他类型:按二进制流下载
}
关键:
Content-Type:HTTP 响应头的核心字段,客户端(如浏览器)根据该字段解析响应体(如text/html则渲染为页面,image/jpeg则显示为图片)- 编码指定:
text/html; charset=UTF-8确保 HTML 中的中文不会乱码
定位静态资源并生成响应
根据解析后的请求路径,拼接本地资源路径,判断资源是否存在,再分别生成 200 成功响应 或 404 失败响应:
资源存在(200 OK 响应)
// 拼接本地资源路径:项目中 resources 目录的绝对路径 + 请求路径
var file = new File("http/src/main/resources" + path);
if (file.exists()) {
// 1. 写入响应行:HTTP/1.1 200 OK(协议版本 + 状态码 + 状态描述)
output.write("HTTP/1.1 200 OK\r\n".getBytes());
// 2. 写入响应头:Content-Type + 空行(响应头与响应体的分隔符)
output.write(("content-type: " + contentType + "\r\n\r\n").getBytes());
// 3. 读取本地文件(静态资源)并写入响应体
var in = new BufferedInputStream(new FileInputStream(file)); // 缓冲流读取文件,提高效率
int len = -1;
var buff = new byte[1024 * 8]; // 8KB 缓冲区:平衡读取效率与内存占用
while((len = in.read(buff)) != -1) { // 循环读取文件内容
output.write(buff, 0, len); // 将读取的内容写入输出流(发送给客户端)
}
in.close(); // 关闭文件输入流
}
示例流程:
- 请求路径:
/pages/static.html - 本地文件路径:
http/src/main/resources/pages/static.html - 响应内容:先发送
HTTP/1.1 200 OK和Content-Type: text/html; charset=UTF-8,再发送 HTML 文件内容
资源不存在(404 Not Found 响应)
else {
// 1. 写入响应行:HTTP/1.1 404 NOT FOUND(客户端请求的资源不存在)
output.write("HTTP/1.1 404 NOT FOUND\r\n".getBytes());
// 2. 写入响应头:指定响应体为 HTML 类型(显示中文提示)
output.write(("content-type: text/html; charset=UTF-8\r\n\r\n").getBytes());
// 3. 写入响应体:404 错误提示文本
output.write("404,你所请求的资源不存在".getBytes());
}
关键:
- 404 状态码:告诉客户端“请求的资源不存在”,是 HTTP 最常见的错误状态码之一
- 响应体:直接返回中文提示,浏览器会按
Content-Type渲染为文本




浙公网安备 33010602011771号