简介:这份资源是华中科技大学计算机科学与技术专业的计算机网络课程实验报告,面向正在学习计算机网络、需要完成Socket编程与网络组建实验的高校学生。报告以docx文档形式呈现,压缩包内共1个文件,大小约3.67MB,内容结构完整,包含实验目的、要求、内容、过程与体会等标准章节。报告涵盖两大部分:一是基于Java Socket的E-Mail编程实验,涉及TCP/UDP Socket分类、客户端-服务器架构、SMTP与POP3等邮件协议,并给出需求分析、界面设计、写信与收信的实现过程;二是网络组建与实施实验,包含组网、路由配置、VLAN划分及访问控制配置四个子实验,涉及网络拓扑结构、路由器与交换机等设备配置及网络测试方法。目前已有427人学习,适合作为课程实验参考与期末复习资料,帮助读者理解网络通信编程与组网配置的关键步骤。
1. 从一份 2013 年的华科实验报告说起:Java Socket 手写邮件客户端到底能跑通什么
翻到这份《华科-计算机网络实验报告-Java_Socket编程-网络组建实验.docx》的时候,我第一反应是:这不是那种交完就删的课程作业,而是一份把应用层协议从零拆到字节流的完整记录。它用 Java 的Socket类直接怼 SMTP 和 POP3 服务器,不借助JavaMail这类现成组件,硬生生把一封邮件的发送、接收、下载、删除全流程走了一遍。对正在啃 socket 网络编程、准备 java 面试题里“TCP 三次握手怎么落到代码”这类问题的同学来说,这份报告的价值不在于界面多漂亮,而在于它把EHLO、AUTH LOGIN、MAIL FROM、RCPT TO、DATA这些命令和服务器返回码一行行打印出来,让你看见协议是怎么呼吸的。它适合两类人:一是刚学完计算机网络、想找个能跑通的 demo 把理论焊死在代码上的学生;二是需要快速回顾 Java 原生 Socket 操作、不想被框架封装遮住眼睛的开发者。下面我就按“这份资源是什么、怎么复现、坑在哪”的顺序,把它拆开讲透。
2. 把 SMTP 会话拆成命令行:SendAction 里每一行代码在跟服务器说什么
2.1 为什么不用 JavaMail 而坚持裸写 Socket
这份报告在实验要求里写得很死:“必须基于 Socket 编程,不能直接借用任何现成的组件。” 这条约束恰恰是它最值得看的地方。JavaMail把Session、Transport、Store封装得太好,好到你写完Transport.send(message)就结束了,根本不知道背后发了什么。而裸写 Socket 意味着你要自己拼邮件报文、自己处理 Base64 编码、自己读服务器应答。常见做法是先用telnet连一下邮件服务器的 25 端口,手动敲一遍EHLO、AUTH LOGIN,看服务器怎么回,再把这一串交互翻译成 Java 代码。报告里SendAction函数就是这么干的:它把发件人、收件人、主题、正文拼成一个字符串,然后通过PrintWriter一行行推给服务器。这种写法在 2024 年看确实原始,但对于理解 SMTP 是“基于文本行的请求-响应协议”这件事,没有比它更直接的了。
2.2 SendAction 的完整调用链与参数含义
先看类里的数据结构,这是后面所有操作的输入源:
public class SendFrame extends JInternalFrame { private JTextField txt_to; // 收件人地址输入框 private JTextField txt_subject; // 主题输入框 private JTextArea textArea; // 正文输入区 private String pusernameadd; // 当前用户完整邮件地址,如 xxx@163.com private String ppasswd; // 邮箱密码或授权码 private String psn_s_smtp; // SMTP 服务器地址,如 smtp.163.com private String pusername1; // 用户名(不含 @ 后缀) }这些字段在构造函数SendFrame里从登录界面传进来,赋值后一直挂在对象上。当用户点击 Send 按钮,btnNewButton.addActionListener触发,调用SendAction(txt_to.getText(), textArea.getText(), txt_subject.getText())。注意参数顺序:收件人、正文、主题——这个顺序和界面上从上到下的排列不一致,写代码时如果按界面顺序传参,主题和正文就会互换,这是第一个容易翻车的地方。
SendAction内部的核心逻辑分四步。第一步拼报文:
mailContent = "From: " + from + "\n" + "To: " + to + "\n" + "Subject: " + subject1 + "\n" + "Constent: " + constent1 + "\n";这里有个明显的笔误:Constent应该是Content,但 SMTP 协议本身不校验头部字段名,服务器只认From、To、Subject这些标准字段,Constent会被当作自定义头忽略掉,正文实际上是通过后面的DATA段单独发送的。所以这个拼写错误不影响功能,但会让你在抓包时困惑一下。
第二步建立连接并读取欢迎语:
String hostName = InetAddress.getLocalHost().getHostName(); Socket s = new Socket(mailServer, 25); BufferedReader inFromServer = new BufferedReader( new InputStreamReader(s.getInputStream())); PrintWriter outToServer = new PrintWriter(s.getOutputStream(), true); response = inFromServer.readLine(); System.out.print("MailServer:" + response + "\n");new Socket(mailServer, 25)这行代码背后是 TCP 三次握手,连上之后服务器会主动推一行220开头的欢迎语。PrintWriter的第二个参数true表示自动 flush,少了它你会发现在readLine()那里卡死——因为数据还在缓冲区里没发出去。
第三步身份验证,这是最容易出问题的地方:
BASE64Encoder encoder = new BASE64Encoder(); String encodedUser = encoder.encode(pusername1.getBytes()); String encodedPwd = encoder.encode(ppasswd.getBytes()); outToServer.println("EHLO " + hostName); for (int i = 0; i < 5; i++) { response = inFromServer.readLine(); System.out.print("MailServer:" + response + "\n"); } outToServer.println("AUTH LOGIN "); response = inFromServer.readLine(); outToServer.println(encodedUser); response = inFromServer.readLine(); outToServer.println(encodedPwd); response = inFromServer.readLine();EHLO之后服务器会返回多行应答,报告里硬编码读 5 行,这个数字因服务器而异。163 邮箱通常返回 5 行左右,但 Gmail 可能更多。如果读少了,后面的AUTH LOGIN应答就会错位;读多了,readLine()会阻塞。稳妥的做法是循环读到某行以空格开头(多行应答的续行标志)或者直接判断返回码。AUTH LOGIN之后服务器分别要用户名和密码的 Base64 编码,注意这里用的是pusername1(不含 @ 的用户名)而不是完整地址,用错了会返回535认证失败。
第四步发信并断开:
outToServer.println("MAIL FROM: <" + from + ">"); response = inFromServer.readLine(); outToServer.println("RCPT TO: <" + to + ">"); response = inFromServer.readLine(); outToServer.println("DATA"); response = inFromServer.readLine(); outToServer.println(mailContent); outToServer.println("."); response = inFromServer.readLine(); s.close();MAIL FROM和RCPT TO后面必须跟尖括号包裹的地址,这是 RFC 5321 的规定。DATA之后服务器返回354,然后你发送正文,最后用一个单独的点号.表示结束。报告里outToServer.println(mailContent)出现了两次,这是重复发送正文的 bug,会导致邮件内容翻倍。正确做法是只发一次,然后紧跟一个.。
2.3 参数怎么改、失败时看什么
如果你要拿这份代码去连自己的邮箱,需要改三个地方:psn_s_smtp改成你的 SMTP 服务器地址(163 是smtp.163.com,QQ 是smtp.qq.com),pusernameadd改成完整邮件地址,ppasswd改成授权码而不是登录密码。现在主流邮箱都要求用授权码,直接用密码会返回535。端口方面,25 端口在很多网络环境下被运营商封了,可以试 465(SSL)或 587(STARTTLS),但这份代码没有实现 SSL 层,所以只能跑 25 端口,这是它的时代局限。
失败时优先看控制台打印的MailServer:那几行。220是连接成功,250是命令接受,334是等待 Base64 输入,235是认证成功,354是开始发正文,221是断开。如果卡在某个readLine()不动,八成是上一条命令发出去后服务器没给预期响应,用telnet手动复现一遍就能定位。
3. POP3 收信与邮件列表渲染:GetMail 怎么把服务器邮件拉进 JTable
3.1 POP3 的三种状态与 getsubmaillist 的取头逻辑
收信部分由GetMail和POP3Client两个类协作完成。GetMail负责界面和邮件列表展示,POP3Client负责跟服务器对话。POP3 协议分三个阶段:授权(USER/PASS)、事务(STAT、LIST、RETR、DELE)、更新(QUIT)。报告里的getsubmaillist()函数对应事务阶段的TOP命令,只拉取邮件头信息:
public void getsubmaillist() throws Exception { POP3Client pop3 = new POP3Client(get_server, get_username, get_passwd); pop3.connect(); int mailCount = pop3.getMailCount(); submaillist = new String[mailCount]; for (int i = 1; i <= mailCount; i++) { String header = pop3.getMailHeader(i); submaillist[i - 1] = header; } pop3.disconnect(); }getMailCount()内部发的是STAT命令,服务器返回+OK 3 12345表示有 3 封邮件、总大小 12345 字节。getMailHeader(i)发的是TOP i 0,只取第 i 封邮件的头部,不取正文。这样做的目的是快速列出收件箱,避免一次性下载所有邮件正文导致界面卡死。报告里maillist是一个String[20][3]的二维数组,分别存标题、发件人、时间,从头部信息里解析出来填进去。
3.2 JTable 的数据绑定与 Show 按钮的事件传递
GetMail的构造函数里,getsubmaillist()和getmaillist()被依次调用,然后把maillist数组直接塞给JTable:
Object[] columnTitle = {"Title", "From", "Time"}; table = new JTable(maillist, columnTitle); table.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); table.setBounds(14, 44, 804, 366); contentPane.add(table.getTableHeader()); contentPane.add(table);JTable用二维数组构造时,第一维是行,第二维是列,列的顺序必须和columnTitle一一对应。报告里maillist[a][2]存的是时间,maillist[a][1]存的是发件人,maillist[a][0]存的是标题,但在Show按钮的事件里传参顺序是new ShowMail(maillist[a][2], maillist[a][1], maillist[a][0], ...),也就是时间、发件人、标题。如果ShowMail构造函数按标题、发件人、时间的顺序接收,这里就会错位。这是第二个容易翻车的地方,建议在传参前先打印一下数组内容确认。
Show按钮的完整逻辑:
btnShow.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { int[] selected = table.getSelectedRows(); int a = selected[0]; DESKTOP_PANE.add(new ShowMail( maillist[a][2], maillist[a][1], maillist[a][0], getmailcontent(a + 1), DESKTOP_PANE, get_usernamelong, get_passwd, get_username, get_smtp, get_server)); } });getSelectedRows()返回选中行的索引数组,selected[0]取第一行。getmailcontent(a + 1)发的是RETR a+1,把第 a+1 封邮件的完整内容拉下来。注意 POP3 的邮件编号从 1 开始,而数组索引从 0 开始,所以这里要加 1。
3.3 DownloadAll 与 Download&Delete 的差异
报告里还有两个按钮:DownloadAll和Download&Delete。前者循环调用RETR把所有邮件保存到本地文件,后者在保存之后额外发一个DELE命令标记删除,最后QUIT时服务器才真正删除。DELE只是打标记,如果在QUIT之前断开连接,删除不会生效。这个机制是 POP3 的“后悔药”,但报告里没有实现异常回滚,如果保存文件时抛了IOException,DELE已经发出去了,邮件就真没了。稳妥的做法是先全部RETR并写文件成功,再统一发DELE。
4. 网络组建实验的配置逻辑:从 VLAN 划分到访问控制列表
4.1 实验环境与拓扑搭建的先后顺序
报告的第二部分“网络组建与实施”覆盖了四个子实验:组网、路由配置、VLAN 划分、访问控制配置。实验环境通常是 Cisco Packet Tracer 或真实交换机路由器。组网实验要求把 PC、交换机、路由器按指定拓扑连起来,配置 IP 地址和子网掩码,验证同网段互通。路由配置实验要求配置静态路由或 RIP,让不同网段互通。VLAN 划分实验要求在同一台交换机上创建多个 VLAN,把端口划分进去,验证广播域隔离。访问控制实验要求用 ACL 限制特定流量。
这四个实验有严格的依赖顺序:先组网通物理层和数据链路层,再配路由通网络层,再划 VLAN 隔离广播域,最后用 ACL 做安全策略。如果顺序反了,比如先配 ACL 再划 VLAN,ACL 绑定的接口可能因为 VLAN 变更而失效。报告里 2.4.2 到 2.4.5 的节次安排就是这个顺序,照着做不会乱。
4.2 VLAN 划分的关键命令与验证方法
以 Cisco 交换机为例,创建 VLAN 并划分端口的典型配置:
Switch> enable Switch# configure terminal Switch(config)# vlan 10 Switch(config-vlan)# name IT Switch(config-vlan)# exit Switch(config)# vlan 20 Switch(config-vlan)# name HR Switch(config-vlan)# exit Switch(config)# interface fastEthernet 0/1 Switch(config-if)# switchport mode access Switch(config-if)# switchport access vlan 10 Switch(config-if)# exit Switch(config)# interface fastEthernet 0/2 Switch(config-if)# switchport mode access Switch(config-if)# switchport access vlan 20 Switch(config-if)# end Switch# show vlan briefswitchport mode access把端口设为接入模式,switchport access vlan 10把它划进 VLAN 10。验证时用show vlan brief看端口归属,然后在同一 VLAN 的两台 PC 之间 ping 通,不同 VLAN 的 PC 之间 ping 不通。如果不同 VLAN 能 ping 通,说明端口没划对,或者交换机之间跑了 VTP 把 VLAN 配置同步乱了。
4.3 访问控制列表的匹配顺序与常见误配
ACL 的规则是从上往下匹配,一旦匹配就执行动作,不再往下看。所以拒绝规则要放在允许规则前面。一个典型的扩展 ACL:
Switch(config)# access-list 100 deny tcp 192.168.10.0 0.0.0.255 192.168.20.0 0.0.0.255 eq 80 Switch(config)# access-list 100 permit ip any any Switch(config)# interface fastEthernet 0/24 Switch(config-if)# ip access-group 100 indeny tcp拒绝 VLAN 10 访问 VLAN 20 的 80 端口,permit ip any any放行其他所有流量。最后一行ip access-group 100 in把 ACL 应用到接口的入方向。常见误配是忘了最后的permit,导致所有流量被隐式拒绝,整个网络不通。另一个坑是 ACL 应用方向搞反,in是进入接口的流量,out是离开接口的流量,配错了规则不生效。
5. 避坑与排查:这份 2013 年的代码放到今天跑要注意什么
5.1 现象:连接 25 端口超时,控制台没有任何输出
原因:现在绝大多数云服务器和家庭宽带都封了 25 端口的出站流量,防止垃圾邮件。报告写于 2013 年,当时 25 端口还能随便用。
解决:换用 465 或 587 端口,但这份代码没有实现 SSL/TLS,直接改端口号会连上但无法完成AUTH LOGIN。要么补上SSLSocketFactory的代码,要么用本地搭建的邮件服务器(如 hMailServer)在局域网内测试。
5.2 现象:AUTH LOGIN之后返回535 Authentication failed
原因:用了邮箱登录密码而不是授权码。163、QQ、Gmail 从 2022 年起全面要求第三方客户端使用授权码。
解决:登录网页邮箱,在设置里开启 SMTP/POP3 服务并生成授权码,把ppasswd换成授权码。注意授权码是 16 位字符串,不要带空格。
5.3 现象:EHLO之后readLine()阻塞,程序卡死
原因:报告里硬编码读 5 行应答,但不同邮件服务器返回的行数不同。163 可能返回 5 行,QQ 返回 6 行,Gmail 返回 4 行。读少了后面的命令错位,读多了阻塞。
解决:改成循环读取,判断每行是否以空格开头(多行应答的续行),或者读到返回码后跟空格的行停止。示例:
String line; while ((line = inFromServer.readLine()) != null) { System.out.println("MailServer: " + line); if (line.length() >= 4 && line.charAt(3) == ' ') { break; } }5.4 现象:邮件发送成功但正文为空或重复
原因:报告里outToServer.println(mailContent)写了两次,导致正文重复。另外mailContent里用了Constent而不是Content,虽然不影响发送但会让头部多一个无效字段。
解决:删掉重复的那行println,把Constent改成Content。正文应该只在DATA之后发送一次,然后紧跟一个单独的点号。
5.5 现象:BASE64Encoder编译报错,提示找不到符号
原因:sun.misc.BASE64Encoder是 JDK 内部类,从 JDK 9 开始被移除,JDK 11 之后完全不可用。
解决:换成java.util.Base64:
String encodedUser = Base64.getEncoder().encodeToString(pusername1.getBytes()); String encodedPwd = Base64.getEncoder().encodeToString(ppasswd.getBytes());注意Base64.getEncoder()默认不加换行,而sun.misc版本每 76 个字符加一个换行。SMTP 的AUTH LOGIN对换行不敏感,但如果你发现认证失败,可以试Base64.getMimeEncoder()。
6. 进阶玩法:把这份报告改成可配置的邮件客户端并验证协议交互
如果你已经跑通了基本流程,下一步可以把硬编码的参数抽成配置文件,让程序支持多邮箱切换。我一般会在项目根目录放一个mail.properties:
smtp.server=smtp.163.com smtp.port=25 pop3.server=pop3.163.com pop3.port=110 username=yourname password=your_auth_code from=yourname@163.com然后在 Java 里用Properties加载:
Properties props = new Properties(); try (InputStream in = new FileInputStream("mail.properties")) { props.load(in); } String smtpServer = props.getProperty("smtp.server"); int smtpPort = Integer.parseInt(props.getProperty("smtp.port"));这样换邮箱只需要改配置文件,不用重新编译。更进一步,可以把SendAction和POP3Client里的System.out.print换成日志框架,把每次会话的请求和响应写到文件里,方便事后排查。我习惯在readLine()之后加一行logger.debug("S: " + response),在println()之前加一行logger.debug("C: " + command),这样抓出来的日志和 Wireshark 抓包能对上。
验证协议交互是否正确的终极方法是同时开 Wireshark 抓包,过滤tcp.port == 25,看 Java 程序发出的每一行和服务器返回的每一行是否和日志一致。如果日志里有但抓包没有,说明PrintWriter没 flush;如果抓包有但日志没有,说明readLine()读到了空行或者阻塞了。这个对照过程能帮你把 Socket 编程里“发送”和“接收”的边界彻底搞清楚。
从那以后我每次写 Socket 相关的代码,都会强制走一遍“先 telnet 手动敲、再写代码、最后抓包对照”的流程,少一步都可能被玄学问题卡半天。希望这份拆解能帮到你。
本文还有配套的精品资源,点击获取