资讯动态

手写SMTP+POP3协议栈:TCP连接与状态机实战

发布时间:2026/10/9 3:00:56 来源:尧图企业网站定制
简介本资源是一套基于Java Web技术实现的模拟邮件系统课程设计项目面向计算机网络、Web开发初学者及高校课程设计实践者聚焦SMTP/POP3协议原理与应用解决邮件收发核心流程的编程实现问题。项目采用MVC架构以Maven管理依赖整合Servlet、JSP、MyBatis与JavaMail API支持用户注册登录、信息维护、发信、收信及回复等完整功能适合作为计算机网络协议实践与Java EE综合实训案例。压缩包含58个文件主体为12个Java源码含协议交互逻辑、12个编译后Class文件、10个XML配置含MyBatis映射与Web部署、6个JSP页面前端交互及配套CSS、Properties等资源总大小仅119KB结构精简便于快速部署调试。已有3493人学习下载提供可直接运行的完整工程含.project、pom.xml、.classpath等IDE识别文件目录层次清晰涵盖src、target、WEB-INF等标准Web模块是理解邮件协议落地与Java Web分层开发的优质教学参考。1. 模拟邮件系统SMTPPOP3协议计算机网络课程设计不是写个网页发邮件而是亲手拆解TCP连接、状态机与协议栈的“黑匣子”你有没有试过在浏览器里点一下“发送”邮件就飞出去了但没人告诉你那背后其实是三次握手建立TCP连接、HELO握手、AUTH认证、MAIL FROM校验、RCPT TO遍历收件人、DATA传输正文、最后QUIT断连——一整套状态机在跑。这个课程设计就是让你把这套“黑匣子”一层层剥开用Java手写SMTP客户端模拟发信流程用POP3命令逐行解析响应码OK / -ERR在Servlet里硬编码状态转换逻辑而不是调一个transport.send()就完事。它不追求高并发或Web UI炫酷而是逼你直面RFC 5321SMTP和RFC 1939POP3的原始字节流——比如为什么CRLF必须是\r\n而不是\n为什么OK后面必须跟空格再接描述为什么RETR 1返回的邮件头里Date:字段要严格按RFC 2822格式。适合计算机网络课刚学完应用层、想把协议从PPT落到代码里的本科生也适合DevOps工程师补足网络协议底层认知——毕竟线上邮件服务出问题时telnet mail.example.com 25敲几条命令比看日志快得多。资源包里没有现成的Docker镜像或一键部署脚本只有可调试的源码、带注释的SQL建表语句、以及一份要求你手动配置MySQL驱动和JavaMail依赖的pom.xml。2. 协议选型与MVC分层为什么不用Spring Boot而坚持ServletJSPMyBatis2.1 为什么死磕原生Servlet而不上Spring Boot这不是技术怀旧是教学约束下的理性选择。课程设计明确要求“体现协议交互细节”而Spring Boot的JavaMailSender封装太深——它自动处理STARTTLS协商、自动重试、自动解析Message-ID你根本看不到EHLO后服务器返回的250-STARTTLS响应码更没法在doPost()里打断点观察socket.getOutputStream().write(MAIL FROM:userlocal\r\n.getBytes())发出后socket.getInputStream().readLine()读到的是250 2.1.0 Ok还是550 User unknown。用原生Servlet你能把HttpServlet的service()方法当成协议状态机的主循环request.getParameter(action)对应SMTP命令send/reply/checkresponse.getWriter()输出HTML页面的同时后台线程正用Socket连接本地SMTP服务器。这种“一手抓HTTP一手抓SMTP”的撕裂感恰恰是理解应用层协议桥接的关键。提示项目中src/main/java/com/webchat/mail/SmtpClient.java不是工具类而是教学载体——它的每个sendCommand()方法都对应RFC 5321的一个步骤注释里直接引用标准原文段落号如“RFC 5321 Section 4.1.1.1”方便你对照查证。2.2 JSP作为View层的真实代价为什么不用Thymeleaf或Vue因为课程设计验收时老师会检查% request.getAttribute(mailList) %是否真的从Servlet传入会要求你解释c:forEach标签在JSP容器里如何被翻译成Java字节码。JSP强制你面对HttpServletRequest的生命周期session.setAttribute(currentUser, user)存的是什么对象request.getRequestDispatcher(/inbox.jsp).forward(request, response)之后response.getWriter()还能不能写这些细节在现代前端框架里被抽象掉了但在这里它们就是协议状态的映射——用户登录成功后session里存的不仅是用户名更是该用户绑定的POP3邮箱凭证点击“查看收件箱”时JSP页面加载触发的jsp:include pageloadInbox.jsp/背后是Pop3Client实例发起LIST命令并解析OK 3 messages响应。用JSP你被迫把HTTP Session和POP3会话状态耦合起来而这正是真实邮件系统中Web Mail Gateway的典型架构。2.3 MyBatis的极简配置为什么不用JDBC Template或纯JDBC看src/main/resources/mybatis-config.xml——它只有environments和mappers两个节点连事务管理器都设为JDBC不走Spring。原因很实在课程设计数据库只存三张表users,mails,smtp_configCRUD操作全在UserMapper.xml里用select/insert硬写SQL。比如注册用户时的插入语句insert idinsertUser parameterTypecom.webchat.entity.User INSERT INTO users (username, password, email, smtp_server, pop3_server, smtp_port, pop3_port) VALUES (#{username}, #{password}, #{email}, #{smtpServer}, #{pop3Server}, #{smtpPort}, #{pop3Port}) /insert注意smtp_port和pop3_port是整型字段但你在JSP表单里填的是字符串25MyBatis自动做类型转换——这省去了你手写PreparedStatement.setInt(6, Integer.parseInt(request.getParameter(smtpPort)))的麻烦又没引入Spring的复杂性。更重要的是mails表结构刻意设计为from_user,to_user,subject,content,send_time完全模仿真实邮件存储逻辑而非关系型范式没建外键没拆附件表因为课程目标是协议模拟不是数据库设计。2.4 JavaMail API的“阉割式”使用为什么只用Transport.send()而不碰Session翻src/main/java/com/webchat/mail/SmtpClient.java你会发现它根本没创建Session对象而是直接调用public void sendMail(String from, String to, String subject, String content) throws Exception { Properties props new Properties(); props.put(mail.smtp.host, smtpHost); props.put(mail.smtp.port, smtpPort); props.put(mail.smtp.auth, true); // 注意这里没设mail.smtp.starttls.enable Session session Session.getInstance(props); Message message new MimeMessage(session); message.setFrom(new InternetAddress(from)); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); message.setSubject(subject); message.setText(content); Transport transport session.getTransport(smtp); transport.connect(smtpHost, username, password); transport.sendMessage(message, message.getAllRecipients()); transport.close(); }为什么删掉STARTTLS因为课程设计运行环境是局域网内自建SMTP服务器见第4章明文传输即可。强行加TLS会引入证书信任链问题而学生根本没时间配OpenSSL。这种“功能裁剪”恰恰体现了工程取舍——真实开发中你也要判断是花3天配通Lets Encrypt还是先用mail.smtp.ssl.enablefalse跑通核心流程这份代码的答案很务实先让协议动起来。3. 核心协议实现SMTP发信与POP3收信的手动状态机编码3.1 SMTP发信流程从Socket连接到DATA块传输的七步拆解别被JavaMail的封装迷惑真正的SMTP交互是字符协议。SmtpClient.java里sendMail()方法实际执行以下步骤已简化异常处理// 步骤1建立TCP连接 Socket socket new Socket(smtpHost, smtpPort); BufferedReader reader new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer new PrintWriter(socket.getOutputStream(), true); // 步骤2等待220服务就绪 String response reader.readLine(); // 应为 220 mail.example.com ESMTP Postfix if (!response.startsWith(220)) throw new RuntimeException(SMTP server not ready); // 步骤3发送HELO/EHLO注意必须用\r\n换行 writer.println(EHLO localhost); response reader.readLine(); // 应为 250-mail.example.com 多行扩展能力 // 步骤4认证若需 writer.println(AUTH LOGIN); response reader.readLine(); // 应为 334 VXNlcm5hbWU6 writer.println(Base64.getEncoder().encodeToString(username.getBytes())); response reader.readLine(); // 应为 334 UGFzc3dvcmQ6 writer.println(Base64.getEncoder().encodeToString(password.getBytes())); // 步骤5指定发件人 writer.println(MAIL FROM: from ); response reader.readLine(); // 应为 250 2.1.0 Ok // 步骤6指定收件人可多个 writer.println(RCPT TO: to ); response reader.readLine(); // 应为 250 2.1.0 Ok // 步骤7传输邮件内容关键以单独的.行结束 writer.println(DATA); response reader.readLine(); // 应为 354 End data with CRLF.CRLF writer.println(From: from); writer.println(To: to); writer.println(Subject: subject); writer.println(); // 空行分隔头与体 writer.println(content); writer.println(.); // 必须独占一行且仅含英文句点 response reader.readLine(); // 应为 250 2.0.0 Ok: queued as ABC123注意每行命令后必须调用reader.readLine()等待服务器响应否则后续命令会堆积在缓冲区。writer.println()自动添加\r\n但如果你用writer.write()就得手动加——这是学生最容易翻车的点writer.write(HELO localhost\n)会失败因为SMTP要求\r\n。3.2 POP3收信流程LIST/RETR/DELE命令的响应解析陷阱POP3比SMTP更“吝啬”服务器响应全是单行文本。Pop3Client.java的checkInbox()方法核心逻辑// 连接后先发USER/PASS认证 writer.println(USER username); response reader.readLine(); // 应为 OK writer.println(PASS password); response reader.readLine(); // 应为 OK // LIST命令获取邮件列表返回格式序号 长度如 1 1234 writer.println(LIST); response reader.readLine(); // 第一行 OK 3 messages ListString mailInfo new ArrayList(); while (!(response reader.readLine()).equals(.)) { if (response ! null !response.trim().isEmpty()) { mailInfo.add(response); // 存 1 1234, 2 5678... } } // RETR命令获取第1封邮件返回完整RFC 2822格式 writer.println(RETR 1); response reader.readLine(); // OK 1234 octets StringBuilder mailContent new StringBuilder(); while (!(response reader.readLine()).equals(.)) { if (response ! null) { mailContent.append(response).append(\n); } } // 注意POP3返回的邮件头可能不含Message-ID需在入库时生成关键陷阱RETR返回的邮件内容里原始CRLF被转义为\r\n但JSP显示时需还原为HTML换行brDELE 1只是标记删除必须QUIT后才真正生效——很多学生测试时发现“删了邮件还在”就是因为没调quit()方法。3.3 MVC控制器中的协议桥接Servlet如何协调HTTP与邮件协议MailServlet.java是整个系统的神经中枢。它不做业务逻辑只做协议路由protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); HttpSession session request.getSession(); switch (action) { case login: // 1. 从MySQL查用户 User user userMapper.selectByUsername(request.getParameter(username)); // 2. 用Pop3Client验证邮箱密码真连POP3服务器 Pop3Client pop3 new Pop3Client(user.getPop3Server(), user.getPop3Port()); boolean authOk pop3.authenticate(user.getUsername(), user.getPassword()); if (authOk) { session.setAttribute(currentUser, user); request.getRequestDispatcher(/inbox.jsp).forward(request, response); } break; case send: // 1. 从session取当前用户含SMTP配置 User currentUser (User) session.getAttribute(currentUser); // 2. 调SmtpClient发信真连SMTP服务器 SmtpClient smtp new SmtpClient( currentUser.getSmtpServer(), currentUser.getSmtpPort(), currentUser.getUsername(), currentUser.getPassword() ); smtp.sendMail( currentUser.getEmail(), request.getParameter(to), request.getParameter(subject), request.getParameter(content) ); break; } }看到没login动作里Servlet先查DB再真实发起POP3连接验证密码——这比单纯比对MD5密码有意义得多因为它验证了用户邮箱账户的真实性。而send动作里SmtpClient的构造参数全部来自DB记录意味着每个用户可以绑定不同的SMTP服务商如张三用QQ邮箱李四用163邮箱。这种设计让课程设计脱离了“假数据演示”具备了真实协议适配能力。3.4 数据库设计的协议映射为什么mails表不存raw邮件而存结构化字段看src/main/resources/sql/create_table.sqlCREATE TABLE mails ( id INT PRIMARY KEY AUTO_INCREMENT, from_user VARCHAR(100) NOT NULL, to_user VARCHAR(100) NOT NULL, subject VARCHAR(200), content TEXT, send_time DATETIME DEFAULT CURRENT_TIMESTAMP, status ENUM(sent, received) DEFAULT sent, user_id INT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) );注意content字段存的是纯文本正文不是Base64编码的原始RFC 2822邮件。原因有二一是课程设计不要求解析MIME多部分邮件附件、HTML正文等二是JSP页面展示需要直接% mail.getContent() %如果存原始邮件你得在Servlet里用MimeMessage解析——这又绕回JavaMail封装违背了“手动协议解析”的初衷。所以SmtpClient.sendMail()在发送前会把用户输入的content直接作为邮件正文传输而Pop3Client.checkInbox()收到原始邮件后在入库前用正则提取Subject:和From:字段Pattern subjectPattern Pattern.compile(Subject:\\s*(.*), Pattern.CASE_INSENSITIVE); Matcher m subjectPattern.matcher(rawMail); String subject m.find() ? m.group(1).trim() : 无主题;这种“协议降级”处理是教学项目特有的务实哲学用最少的代码覆盖最核心的协议要素。4. 本地环境搭建CentOS 7下自建SMTP/POP3服务器的三步法4.1 为什么必须自建邮件服务器公网邮箱为何不可用课程设计要求“模拟邮件系统”但如果你直接配smtp.qq.com:587会立刻遇到三个坎TLS强制QQ邮箱要求STARTTLS而项目代码默认明文OAuth2替代密码2023年后QQ邮箱禁用SMTP密码登录必须用授权码而JavaMail的OAuth2支持需额外jar包IP限频公网SMTP服务器对未认证IP每分钟只允许3封邮件测试时频繁失败。所以必须在本地搭一个“裸协议”服务器——不加密、不鉴权、不反垃圾只为让你看清250和550的区别。CentOS 7是最佳选择postfix和dovecot官方仓库稳定配置文件路径统一/etc/postfix/,/etc/dovecot/且与课程设计的Linux部署场景一致避免Windows下telnet不支持Ctrl]退出的玄学问题。4.2 Postfix SMTP服务器三行命令完成基础配置# 1. 安装并启动 sudo yum install -y postfix sudo systemctl enable postfix sudo systemctl start postfix # 2. 关键配置编辑 /etc/postfix/main.cf inet_interfaces loopback-only # 只监听127.0.0.1不暴露公网 mydestination $myhostname, localhost.$mydomain, localhost mynetworks 127.0.0.0/8 # 允许本地网络发信 smtpd_recipient_restrictions permit_mynetworks, reject_unauth_destination # 3. 重启并验证端口 sudo systemctl restart postfix sudo ss -tlnp | grep :25 # 应显示 postmap(1234)提示mydestination必须包含localhost否则RCPT TO:testlocalhost会被拒绝。mynetworks设为127.0.0.0/8后你的Java程序用new Socket(127.0.0.1, 25)就能连上——这才是课程设计要的“可控环境”。4.3 Dovecot POP3服务器让收件箱真正可读# 1. 安装并启动 sudo yum install -y dovecot sudo systemctl enable dovecot sudo systemctl start dovecot # 2. 配置POP3编辑 /etc/dovecot/conf.d/10-mail.conf mail_location mbox:~/mail:INBOX/var/mail/%u # 3. 允许明文登录编辑 /etc/dovecot/conf.d/10-auth.conf disable_plaintext_auth no !include auth-system.conf.ext # 启用系统用户认证 # 4. 开放POP3端口编辑 /etc/dovecot/conf.d/10-master.conf service pop3 { inet_listener pop3 { port 110 } } # 5. 重启并验证 sudo systemctl restart dovecot echo user1 | sudo tee -a /etc/passwd # 创建系统用户密码用passwd命令设 telnet 127.0.0.1 110 # 应返回 OK Dovecot ready.关键点mail_location设为mbox格式因为Dovecot默认用Maildir而课程设计的Pop3Client解析的是传统mbox所有邮件拼在一个文件里用From行分隔。disable_plaintext_auth no是必须的否则USER user1命令会直接被拒绝。4.4 MySQL数据库初始化字符集与权限的血泪经验-- 创建数据库必须用utf8mb4 CREATE DATABASE webchat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建用户并授权不要用root CREATE USER webchatlocalhost IDENTIFIED BY WebChat2024!; GRANT ALL PRIVILEGES ON webchat.* TO webchatlocalhost; FLUSH PRIVILEGES; -- 执行建表SQL来自资源包的 create_table.sql USE webchat; SOURCE /path/to/create_table.sql;注意utf8mb4是唯一能存emoji的字符集而课程设计的邮件主题可能含中文标点。如果用utf8MySQL的伪utf8INSERT INTO mails(subject) VALUES(你好);会变成乱码。另外GRANT语句里webchatlocalhost的localhost不能写成%否则Java连接时会因DNS解析失败报UnknownHostException——这是CentOS下最隐蔽的坑。5. 避坑指南五个让课程设计卡壳三天的边界问题5.1 现象JSP页面中文乱码但控制台打印正常原因Tomcat默认用ISO-8859-1解码GET参数而JSP页面% page contentTypetext/html;charsetUTF-8%只影响输出不影响请求参数。解决在web.xml中配置CharacterEncodingFilter或在每个Servlet开头加request.setCharacterEncoding(UTF-8)。更彻底的方案是在server.xml的Connector节点加URIEncodingUTF-8。5.2 现象SmtpClient连接本地Postfix时抛Connection refused原因Postfix默认监听127.0.0.1:25但Java程序里写的new Socket(localhost, 25)在CentOS下可能解析为::1IPv6地址而Postfix没监听IPv6。解决将代码中的localhost改为127.0.0.1或在/etc/hosts里注释掉::1 localhost这一行。5.3 现象POP3RETR命令返回的邮件内容缺失首行原因reader.readLine()会吃掉行尾的\r\n而POP3规范要求RETR返回的邮件头必须保留原始CRLF。当RETR返回From: ab.com\r\nTo: cd.com\r\n\r\nHello时readLine()第一次返回From: ab.com第二次返回To: cd.com第三次返回空字符串——导致正文Hello被当作新邮件的From头。解决不用readLine()改用InputStream.read()逐字节读遇到\r\n.\r\n才停止。课程设计包里的Pop3Client.java已修复此问题但很多学生自己重写时会踩坑。5.4 现象MySQL插入用户时报Data truncation错误原因users表的email字段定义为VARCHAR(50)但学生测试时填了very.long.email.addressexample.com超50字符。解决修改建表SQL将email字段扩到VARCHAR(100)。同理subject字段至少设为VARCHAR(200)——RFC 2822允许主题长达998字符但课程设计取200足够。5.5 现象mvn clean package失败提示java.lang.NoClassDefFoundError: javax/mail/Authenticator原因pom.xml里javax.mail依赖范围是provided假设容器提供但Tomcat 8不自带JavaMail需改为compile。解决修改pom.xmldependency groupIdcom.sun.mail/groupId artifactIdjavax.mail/artifactId version1.6.2/version scopecompile/scope !-- 原为 provided -- /dependency6. 进阶验证技巧用telnet和Wireshark给你的协议实现做“尸检”6.1 telnet协议交互的终极调试器别急着跑Java程序先用telnet手动走一遍流程这是验证服务器配置是否正确的黄金标准。以发信为例$ telnet 127.0.0.1 25 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is ^]. 220 mail.example.com ESMTP Postfix # 服务就绪 EHLO localhost # 发起握手 250-mail.example.com 250-PIPELINING 250-SIZE 10240000 250-VRFY 250-ETRN 250-STARTTLS 250-ENHANCEDSTATUSCODES 250-8BITMIME 250 DSN MAIL FROM:testlocalhost # 指定发件人 250 2.1.0 Ok RCPT TO:testlocalhost # 指定收件人 250 2.1.0 Ok DATA # 开始传内容 354 End data with CRLF.CRLF Subject: Test from telnet From: testlocalhost Hello, this is a test mail. . 250 2.0.0 Ok: queued as 123ABC QUIT # 断连 221 Bye Connection closed by foreign host.提示telnet里按Ctrl]进入命令模式再输quit退出。重点观察每条命令后的三位数响应码——2xx成功4xx临时错误5xx永久错误。如果RCPT TO返回550 User unknown说明Postfix没配好mydestination如果DATA后卡住可能是mynetworks没包含你的IP。6.2 Wireshark抓包看清TCP流里的真实字节启动Wireshark过滤tcp.port 25 || tcp.port 110然后运行你的Java程序发一封邮件。展开TCP流你会看到0000 45 00 01 f4 00 00 40 00 40 06 00 00 7f 00 00 01 E............. 0010 7f 00 00 01 c0 0e 00 19 00 00 00 00 00 00 00 00 ................ ... 00c0 4d 41 49 4c 20 46 52 4f 4d 3a 3c 74 65 73 74 40 MAIL FROM:test 00d0 6c 6f 63 61 6c 68 6f 73 74 3e 0d 0a localhost..00c0行的4d 41 49 4c...是MAIL FROM:testlocalhost的ASCII十六进制0d 0a就是\r\n。对比你的Java代码里writer.println(MAIL FROM:testlocalhost)确认它真的发出了正确字节。如果看到0a\n而没有0d\r说明你用了writer.write()没加\r——这就是Wireshark能揪出的底层错误。6.3 数据库邮件表的“协议证据链”验证表字段名期望值验证方式协议意义from_usertestlocalhostSELECT from_user FROM mails WHERE id1;对应SMTPMAIL FROM命令的参数to_usertestlocalhost同上对应SMTPRCPT TO命令的参数send_time接近发信时刻SELECT NOW(), send_time FROM mails WHERE id1;证明邮件已成功入队Postfix日志可佐证statussent同上区分发信与收信收信时statusreceiveduser_id大于0的整数SELECT user_id FROM mails WHERE id1;绑定到users表实现用户隔离这张表是你协议实现的“结案报告”。当statussent且send_time准确说明SMTP流程走通当Pop3Client从Dovecot拉取邮件后mails表新增记录且statusreceived说明POP3流程闭环。没有这个表你的“模拟”只是内存里的幻觉。从那以后我每次教学生做网络协议课程设计都强制他们先用telnet连三次——一次连通一次发HELO一次发MAIL FROM。只有亲眼看到250响应码从屏幕上滚出来他们才相信自己真的在和协议对话而不是在调API。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑