16.过滤器中处理异常响应返回

过滤器中处理异常响应返回

适用模块:ai-springboot-new(心理健康助手后端·新版)

涉及包:util

技术栈:Spring Boot 4.1.1 + Spring Security 6.x + Auth0 java-jwt 4.4.0 + Hutool 5.x

关联文档:《JWT 认证与 Token 创建方法实现》、《创建获取用户接口与 JWT 认证器》、《引入 Spring Security 安全框架与授权规则配置》


1. 背景与目标

在《创建获取用户接口与 JWT 认证器》中,JwtAuthticationFilter 已经挂载进 Spring Security 过滤链,但 doFilterInternal 只会打印日志------既不提取 Token,也不拒绝非法请求,属于"管道通了但没通水"的状态。

本次改动补齐了拒绝分支 的最小闭环:当请求未携带 Token 时,不再放行到后续链路,而是当场返回统一的 401 JSON 响应 。同时抽出可复用的响应工具类,并把 Token 提取逻辑收敛到 JwtTokenUtil

目标 实现方式 本次状态
过滤器内统一输出错误响应 新增 ResponseUtil.writeError(response, resultCode) ✅ 已落地
ResultCode → HTTP 状态码映射 switch 表达式按枚举分派 401 / 403 / 400 ✅ 已落地
Token 提取集中管理 JwtTokenUtil.extractTokenFromRequest(request) ⚠️ 骨架已建,解析逻辑未实现
未携带 Token 时拒绝请求 else 分支清理上下文 + 写回 401 ⚠️ 已写响应,但未中断链路

说明:本次解决的是"过滤器里怎么把错误返回给前端 "这一问题。之所以必须单独处理,是因为过滤器执行在 DispatcherServlet 之前,@RestControllerAdvice 的全局异常处理器根本拦截不到 这一层抛出的异常或拒绝行为,只能手工操作 HttpServletResponse


2. 本次改动文件一览

文件路径 类型 说明
src/main/java/com/example/aispringbootnew/util/ResponseUtil.java 新增 过滤器专用响应工具类,按 ResultCode 写回统一 JSON 错误体(本次核心)
src/main/java/com/example/aispringbootnew/util/JwtTokenUtil.java 修改 新增 extractTokenFromRequest,统一从请求头提取 Token
src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java 修改 新增 Token 提取与拒绝分支,接入 ResponseUtil

3. 核心实现一:ResponseUtil ------ 过滤器专用响应工具

3.1 为什么不能复用全局异常处理器

Spring MVC 的异常处理机制存在一个处理边界

复制代码
HTTP 请求
   │
   ▼
Servlet 过滤器链(JwtAuthticationFilter 在这一层)
   │   ← 这里抛出的异常 / 主动拒绝,@RestControllerAdvice 完全看不到
   ▼
DispatcherServlet
   │
   ▼
HandlerMapping → Controller
   │   ← 只有从这里开始抛出的异常,才会被 @RestControllerAdvice 捕获
   ▼
HandlerExceptionResolver(@RestControllerAdvice 的生效位置)

@RestControllerAdvice 本质上是一个 HandlerExceptionResolver,它的注册与触发都发生在 DispatcherServlet 内部。过滤器阶段请求尚未进入 MVC 容器 ,此时既没有 HandlerMethod,也没有 ModelAndView 概念,异常处理链路无从谈起。

因此过滤器要拒绝请求,只有两条路:

方式 做法 适用性
抛异常交给上游 抛出异常让 Servlet 容器返回错误页 ❌ 返回格式与 Result 不统一,前端无法解析
手写响应 直接操作 HttpServletResponse 写入 JSON ✅ 本次采用

3.2 完整实现

14:36:src/main/java/com/example/aispringbootnew/util/ResponseUtil.java 复制代码
public class ResponseUtil {
    public static void writeError(HttpServletResponse response, ResultCode resultCode) {
        // 根据不同结果码返回不同的响应
        int status = switch (resultCode) {
            case UNAUTHORIZED, ACCESS_UNAUTHORIZED, TOKEN_INVALID, TOKEN_BLOCKED, TOKEN_EXPIRED ->
                    HttpStatus.UNAUTHORIZED.value();
            case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value();
            default -> HttpStatus.BAD_REQUEST.value();
        };

        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());

        try (PrintWriter writer = response.getWriter()) {
            String jsonResponse = JSONUtil.toJsonStr(Result.error(resultCode.getCode(), resultCode.getMsg(),  null));
            writer.write(jsonResponse);
            writer.flush(); // 确保将相应内容写入到客户端浏览器
        } catch (IOException e) {
            System.out.println("写入响应失败:" + e.getMessage());
        }
    }
}

3.3 分步骤拆解

① 结果码 → HTTP 状态码映射

17:22:src/main/java/com/example/aispringbootnew/util/ResponseUtil.java 复制代码
int status = switch (resultCode) {
    case UNAUTHORIZED, ACCESS_UNAUTHORIZED, TOKEN_INVALID, TOKEN_BLOCKED, TOKEN_EXPIRED ->
            HttpStatus.UNAUTHORIZED.value();
    case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value();
    default -> HttpStatus.BAD_REQUEST.value();
};

这里关键是理解两层状态码的差异,它们服务于不同消费者:

层级 载体 取值示例 消费者
HTTP 状态码 response.setStatus(...) 401 / 403 / 400 浏览器、网关、前端拦截器、监控
业务状态码 Result.code A0301 / A0230 / A0231 前端业务逻辑

映射分组的设计意图:

分组 对应 ResultCode HTTP 语义
未认证 UNAUTHORIZEDACCESS_UNAUTHORIZEDTOKEN_INVALIDTOKEN_BLOCKEDTOKEN_EXPIRED 401 "你没有有效身份,去登录/换 Token"
无权限 TOKEN_ACCESS_FORBIDDEN 403 "身份有效,但被禁止访问"
兜底 其余所有枚举 400 避免未知结果码造出非法 HTTP 状态

注意 TOKEN_INVALID / TOKEN_EXPIRED / TOKEN_BLOCKEDResultCode 中的 code 都是 "A0230",属于"同一类但细分原因"的设计:HTTP 层统一 401,前端靠 msg 区分具体原因。

另需注意 switch 表达式(Java 14+ 箭头语法)要求枚举常量名完整列出 ,且 default 不可省略------一旦 ResultCode 新增枚举项,会静默落入 400 分支,不会编译报错。

② 设置响应头

24:26:src/main/java/com/example/aispringbootnew/util/ResponseUtil.java 复制代码
response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());

三步缺一不可:

  • setStatus必须显式设置 。若只写响应体不设状态码,HTTP 状态仍是默认的 200,前端 axios 拦截器不会走 error 分支,等于"业务失败了但看起来像成功"。
  • setContentType:声明 application/json,否则浏览器/前端可能按 text/htmltext/plain 解析。
  • setCharacterEncodingUTF-8 必须显式指定 。默认编码为 ISO-8859-1,而 msg 全为中文(如"访问未授权"),不设置必然乱码。

③ 序列化并写回

28:34:src/main/java/com/example/aispringbootnew/util/ResponseUtil.java 复制代码
try (PrintWriter writer = response.getWriter()) {
    String jsonResponse = JSONUtil.toJsonStr(Result.error(resultCode.getCode(), resultCode.getMsg(),  null));
    writer.write(jsonResponse);
    writer.flush(); // 确保将相应内容写入到客户端浏览器
} catch (IOException e) {
    System.out.println("写入响应失败:" + e.getMessage());
}
  • 复用 Result.error(code, msg, data),保证过滤器返回的错误体与 Controller 返回的成功体结构完全一致,前端只需一套解析逻辑;
  • JSONUtil 来自 Hutool,与 Result 序列化在 MVC 中使用的 Jackson 结果等价,避免手动拼接字符串;
  • try-with-resources 自动关闭 PrintWriter ------ 关闭时会隐式 flush,因此这里手动 flush 是"双保险";
  • 捕获 IOException 后仅打印日志,没有继续抛出:响应阶段已无更上层的兜底手段,抛出只会污染异常栈。

3.4 返回体示例

ACCESS_UNAUTHORIZEDcode = "A0301"msg = "访问未授权")为例:

http 复制代码
HTTP/1.1 401 Unauthorized
Content-Type: application/json;charset=UTF-8
json 复制代码
{
  "code": "A0301",
  "msg": "访问未授权",
  "data": null
}

对比白名单接口的正常响应:

json 复制代码
{ "code": "200", "msg": "操作成功", "data": null }

两者仅 code / msg 不同,data 字段始终存在------前端可统一按 res.data.code === '200' 判定成败。


4. 核心实现二:JwtTokenUtil.extractTokenFromRequest

4.1 实现代码

54:66:src/main/java/com/example/aispringbootnew/util/JwtTokenUtil.java 复制代码
// 提取token
public static String extractTokenFromRequest(HttpServletRequest request) {
    if (request == null) {
        return null;
    }

    String tokenHeader = request.getHeader("token");
    if (tokenHeader == null) {

    }

    return null;
}

4.2 设计意图

设计点 说明
方法定位为 static 与同类的 generateToken 保持一致,使用方(过滤器)无需注入即可调用
入参判空 request == null 直接返回 null,把判空责任收敛在工具类内,调用方只管"有没有拿到 Token"
返回值约定 返回 null 统一表示"无可用的 Token",由调用方决定如何响应

4.3 与旧版实现的对照

旧项目 ai-spingboot 中该方法的完整实现为:

java 复制代码
String tokenHeader = request.getHeader("token");
if (StringUtils.hasText(tokenHeader)) {
    return tokenHeader;
}
return null;

对比可知本次移植只搬了骨架、漏了两处

差异 新版现状 旧版做法 影响
if 分支为空 if (tokenHeader == null) { } if (StringUtils.hasText(tokenHeader)) { return tokenHeader; } 命中条件后不做任何事,直接落到 return null
恒返回 null 无论有无 Token 都返回 null 有值即返回 过滤器永远走"未携带 Token"分支

详见第 7 节问题 ②。


5. 核心实现三:JwtAuthticationFilter 拒绝分支

5.1 完整实现

23:49:src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java 复制代码
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 获取请求的URL和方法
    String requestURI = request.getRequestURI();
    String requestMethod = request.getMethod();
    System.out.printf("requestURI: %s\n", requestURI);
    System.out.printf("requestMethod: %s\n", requestMethod);

    // 提取 JWT token
    String token = JwtTokenUtil.extractTokenFromRequest(request);
    if (StringUtils.hasText(token)) {

    } else {
        // 清理上下文
        clearSecurityContext();
        ResponseUtil.writeError(response, ResultCode.ACCESS_UNAUTHORIZED);
    }

    // 继续过滤器链
    filterChain.doFilter(request, response);
}

// 清理上下文
private void clearSecurityContext() {
    SecurityContextHolder.clearContext();
}

5.2 三分支结构对照

改动后 doFilterInternal 的骨架与旧版项目结构一致,即"有 Token 走验签、无 Token 立即拒绝":

复制代码
doFilterInternal
   │
   ├─ token = extractTokenFromRequest(request)
   │
   ├─ token 非空(StringUtils.hasText)
   │     └─ ① 验签 ② 回查用户 ③ 写入 SecurityContext   ← 本次仍留空,待后续实现
   │
   └─ token 为空
         ├─ clearSecurityContext()                      ← 清理脏上下文
         ├─ ResponseUtil.writeError(401)                ← 写回统一错误体
         └─ ❌ 缺少 return                                 ← 见第 7 节问题 ①

   filterChain.doFilter(request, response)               ← 无条件放行

5.3 为什么要先 clearSecurityContext()

SecurityContextHolder 默认使用 ThreadLocal 存储认证信息(SecurityContextHolder.MODE_THREADLOCAL)。Tomcat 使用线程池 复用工作线程,若某个请求异常退出而上下文未清理,残留的 Authentication 可能被下一个复用该线程的请求读到,造成身份串号

本项目 SecurityConfig 中已配置 SessionCreationPolicy.STATELESS,Spring Security 的 SecurityContextPersistenceFilter 会在请求结束时常规清理;但在过滤器主动拒绝 的分支里显式调用 SecurityContextHolder.clearContext(),是"明确表达此处不应存在任何身份"的低成本防御,也便于后续在同一分支追加更多拒绝条件时复用。

5.4 一次无 Token 请求的时序(当前实际行为)

复制代码
客户端 GET /api/user/current(不带 token 头)
   │
   ▼
shouldNotFilter → /api/user/current 不在 PUBLIC_PATHS → false(进入过滤器)
   │
   ▼
doFilterInternal
   ├─ extractTokenFromRequest → null
   ├─ StringUtils.hasText(null) → false → else 分支
   │     ├─ SecurityContextHolder.clearContext()
   │     └─ ResponseUtil.writeError(response, ACCESS_UNAUTHORIZED)
   │           → response.setStatus(401)
   │           → response.getWriter().write("{...}") 且已 flush
   │
   └─ ⚠️ 未 return → 继续执行 filterChain.doFilter(request, response)
         │
         ▼
   AuthorizationFilter 执行 anyRequest().authenticated()
         └─ SecurityContext 无身份 → 异常 → 尝试写入 401/403 响应
               └─ 响应已提交(committed),写入抛 IllegalStateException

这正是第 7 节问题 ① 的成因:写回了错误响应,却没有中断链条


6. 关键原理:过滤器里失败后必须 return

过滤器与 Controller 的失败处理有本质区别:

维度 Controller / Service Filter
失败表达 throw new BusinessException(...) 手写 response
中断方式 异常向上抛,MVC 停止后续处理 必须显式 return,否则继续走链
统一处理 @RestControllerAdvice 自动兜底 无兜底,全靠自己

filterChain.doFilter(request, response) 的语义是"把请求交给链上的下一个过滤器/最终 Servlet "。它是一次普通方法调用,不是 return 语句的替代品。因此在拒绝分支里:

java 复制代码
} else {
    clearSecurityContext();
    ResponseUtil.writeError(response, ResultCode.ACCESS_UNAUTHORIZED);
    return;   // ← 关键:中断当前过滤器,不再向下传递
}

缺少 return 时,ResponseUtil 已经把 PrintWriter 写出并 flush,HTTP 响应进入 committed 状态。此后链上的 AuthorizationFilter 因身份缺失再想写响应,会因响应已提交而抛 IllegalStateException,日志中出现 getWriter() has already been calledCannot call sendError() after the response has been committed 之类的报错;在部分容器下还可能表现为响应体被截断或状态码不稳定。

旧项目 ai-spingboot 的同一分支写的是:

java 复制代码
clearSecurityContext();
ResponseUtil.writeError(response, ResultCode.ACCESS_UNAUTHORIZED);
return;    // ← 有 return

本次移植时漏掉了 return,需补回。


7. 现存问题与风险

按严重程度排列,前两项直接影响功能可用性

① 拒绝分支缺少 return,错误响应会被后续链路覆盖(致命)

36:42:src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java 复制代码
} else {
    // 清理上下文
    clearSecurityContext();
    ResponseUtil.writeError(response, ResultCode.ACCESS_UNAUTHORIZED);
}

// 继续过滤器链
filterChain.doFilter(request, response);

else 分支写回 401 后没有 returnfilterChain.doFilter(...) 仍会执行。后果:

  1. 请求继续进入受保护接口的处理链路,拒绝形同虚设;
  2. 响应已于 ResponseUtil 中 flush、进入 committed 状态,后续 AuthorizationFilter 再写响应会抛 IllegalStateException,表现为 500 或响应体异常。

修复:

java 复制代码
} else {
    clearSecurityContext();
    ResponseUtil.writeError(response, ResultCode.ACCESS_UNAUTHORIZED);
    return;   // 中断过滤器链,避免响应被覆盖
}

extractTokenFromRequest 恒返回 null,所有受保护请求必被拒(致命)

54:66:src/main/java/com/example/aispringbootnew/util/JwtTokenUtil.java 复制代码
public static String extractTokenFromRequest(HttpServletRequest request) {
    if (request == null) {
        return null;
    }

    String tokenHeader = request.getHeader("token");
    if (tokenHeader == null) {

    }

    return null;
}

两个缺陷叠加:

  • if (tokenHeader == null) { }空分支,写了一行注释都没有,实际什么都没做;
  • 方法末尾无条件 return null,即使读到了 token 头也不会返回。

结果是 StringUtils.hasText(token) 恒为 false,过滤器永远走拒绝分支。修复(对齐旧版实现):

java 复制代码
String tokenHeader = request.getHeader("token");
if (StringUtils.hasText(tokenHeader)) {
    return tokenHeader;
}
return null;

注意需在 JwtTokenUtil 中补上 import org.springframework.util.StringUtils;

③ 请求头名硬编码,未消费 JwtConfig(中)

60:60:src/main/java/com/example/aispringbootnew/util/JwtTokenUtil.java 复制代码
String tokenHeader = request.getHeader("token");

application.yml 中已配置 jwt.header: Authorizationjwt.token-prefix: "Bearer "JwtConfig 也已声明 header / tokenPrefix 字段,但此处直接硬编码了 "token"

问题 后果
头名硬编码 前端按 Authorization: Bearer xxx 发送时取不到,前后端约定失效
未剥离前缀 直接返回 Bearer xxx 会导致后续 JWT 解析 Invalid character

建议改为读取配置(同类已有 getJwtConfig() 私有方法可复用):

java 复制代码
JwtConfig jwtConfig = getJwtConfig();
String header = request.getHeader(jwtConfig.getHeader());   // Authorization
if (StringUtils.hasText(header) && header.startsWith(jwtConfig.getTokenPrefix())) {
    return header.substring(jwtConfig.getTokenPrefix().length());
}
return null;

注意 JwtConfig 本身还缺 @ConfigurationProperties(prefix = "jwt"),配置项当前未绑定,需一并修复(详见《JWT 认证与 Token 创建方法实现》第 9 节问题 ①)。

④ 有 Token 的分支为空,验签链路未闭环(中)

33:34:src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java 复制代码
if (StringUtils.hasText(token)) {

}

即便修好问题 ②,拿到 Token 也只是"什么都不做",SecurityContext 依然空,anyRequest().authenticated() 仍会拒绝。需补齐旧版的完整流程:

java 复制代码
JwtTokenUtil.TokenVerificationResult validationResult = JwtTokenUtil.validateToken(token);
if (validationResult != null && validationResult.isValid()) {
    UserLoginResponseDTO.UserDetailResponseDTO user = userService.getUserById(validationResult.getUserId());
    if (user != null && UserStatus.NORMAL.getCode().equals(user.getStatus())) {
        List<SimpleGrantedAuthority> authorities = Collections.singletonList(
                new SimpleGrantedAuthority("ROLE_" + validationResult.getRoleType()));
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(validationResult.getUsername(), null, authorities));
    } else {
        clearSecurityContext();
        ResponseUtil.writeError(response, ResultCode.TOKEN_ACCESS_FORBIDDEN);
        return;
    }
} else {
    clearSecurityContext();
    ResponseUtil.writeError(response, ResultCode.TOKEN_INVALID);
    return;
}

配套还需在 JwtTokenUtil 中实现 validateToken / verifyToken,并在 JwtAuthticationFilter 中注入 UserService@Resource)。

另外注意:JwtAuthticationFilter 是由 SecurityConfignew JwtAuthticationFilter() 手工创建的 Bean,可以 享受 Spring 注入;但若在 SecurityFilterChain 中直接 new 而非调用 jwtAuthticationFilter()@Resource 将不会生效。

⑤ 调试输出未替换为日志(低)

ResponseUtil 中的 System.out.println("写入响应失败:" + e.getMessage()) 与过滤器中的 System.out.printf 均应替换为 SLF4J 日志,并建议在写响应失败时至少记录异常对象 而非仅 getMessage()

java 复制代码
private static final Logger log = LoggerFactory.getLogger(ResponseUtil.class);
// ...
} catch (IOException e) {
    log.error("写入过滤器错误响应失败", e);
}

⑥ 缺少 Content-Length / 响应体重复写入防护(低)

writeError 未校验 response.isCommitted()。若在已提交的响应上再次调用(例如某个上层过滤器已经写过响应),getWriter() 会直接抛 IllegalStateException。可在方法开头加一道防御:

java 复制代码
if (response.isCommitted()) {
    return;
}

8. 后续演进建议

  1. 修复第 7 节问题 ① :在拒绝分支补 return,这是让"拒绝"真正生效的最小改动,优先级最高。
  2. 修复问题 ② :补全 extractTokenFromRequestif 分支与返回值,使其真正返回请求头中的 Token。
  3. 修复问题 ③ :改为读取 jwt.header / jwt.tokenPrefix,并同步修复 JwtConfig 缺少 prefix 的问题,让配置真正生效。
  4. 完成问题 ④ 的验签链路JwtTokenUtilvalidateToken / verifyToken,过滤器 if (hasText(token)) 分支完成"验签 → 回查用户状态 → 写入 SecurityContext",失败分支同样 ResponseUtil.writeError(...) + return
  5. 统一错误响应出口 :将 ResponseUtil 作为过滤器中唯一的响应出口 ,所有拒绝场景(未携带、验签失败、用户禁用、无权限)都走它,避免格式分叉;也可考虑同时设置 Result 与 HTTP 状态码的映射表,便于后续扩展。
  6. 日志与监控 :把 System.out 全面替换为日志框架;对 401/403 拒绝次数打点,便于发现token 过期集中或撞库尝试。
  7. ResponseUtil 泛化 :当前仅支持错误响应,可扩展 writeSuccess(response, data),使其成为独立于 MVC 的通用响应工具,供其他非 MVC 场景(如定时任务回调、SSE)复用。

9. 涉及文件清单

文件路径 类型 说明
src/main/java/com/example/aispringbootnew/util/ResponseUtil.java 新增 过滤器专用响应工具,ResultCode → HTTP 状态码映射 + 统一 JSON 写回(本次核心)
src/main/java/com/example/aispringbootnew/util/JwtTokenUtil.java 修改 新增 extractTokenFromRequest,当前为待补齐骨架
src/main/java/com/example/aispringbootnew/util/JwtAuthticationFilter.java 修改 新增 Token 提取与拒绝分支,接入 ResponseUtil(缺 return
src/main/java/com/example/aispringbootnew/common/Result.java 已有 统一返回结构,ResponseUtil 复用其 error(code, msg, data)
src/main/java/com/example/aispringbootnew/common/ResultCode.java 已有 状态码枚举,ResponseUtil 的映射输入
src/main/java/com/example/aispringbootnew/config/JwtConfig.java 已有 提供 header / tokenPrefix(尚缺 prefix,且过滤器未消费)
src/main/java/com/example/aispringbootnew/config/SecurityConfig.java 已有 注册并挂载 JwtAuthticationFilter,配置白名单与兜底授权
相关推荐
Java_2017_csdn1 小时前
StringUtils.hasText() 和 StringUtils.isNotBlank() 方法对比
java
Joy T1 小时前
Spring AI 2.0 进阶入门:RAG、Structured Output 与 Agent 信息闭环
java·人工智能·spring·rag·springai·agent入门
xiaoqiMikko2 小时前
jackson-databind 又出 4 条,Dependabot 一条都不报:2.21.5 还差 3 条
java·安全
無限進步D2 小时前
VSCode 简介+安装+JavaWeb前端常用插件+简单配置
java·开发语言·css·vscode·html·css3·插件
xiaoqiMikko2 小时前
Central 上的 Spring 5.3.41 不是官方发的:逐字节比对,官方修两条它少一条
java·安全
用户094248568032 小时前
第8章:Java 内存模型(JMM)入门——可见性、有序性、原子性
java·jvm
AI深栈2 小时前
第 9 章 · Model Context Protocol(MCP)
java·人工智能
雨辰AI2 小时前
信创多租户项目 9 大踩坑|数据隔离失效、权限越权终极解决(金仓 / 达梦 / 高斯全库适配)
java·大数据·数据库·后端
captain3762 小时前
网络原理(4)-TCP ▲▲▲
java·服务器·网络·网络协议·tcp/ip