一步一步从原理跟我学邮件收取及发送 4.不同平台下的socket

时间:2021-11-03 13:29:44

既然是面向程序员的文章那当然不能只说说原理,一定要有实际动手的操作.
    其实作为我个人的经历来说,对于网络编程,这是最重要的一章!

作为一位混迹业内近20年的快退休的程序员,我学习过很多的开发语言和程序类型,比如:pascal,c,c++,delphi,vc,java,kjava,symbian .... objectc,ios ..直到最近还因为工作的关系还得研究前端用的 js (虽然早就学过).列这么多我不是炫耀,也是不要反映老程序员的悲哀历史,我是想通过这些学习到应用的经历告诉大家,其实写什么程序都并不难,难的都是入门. 比如 js/css 我就多次过其门而不入,因为我长期从事后端和 pc 端的工作,web 前端之前并没有真正的应用过,一直就没真正入门.要真正的入门,每种语言或工作环境的也是不同,C 语言是易学难精,能做出一个 hello world 输出就可以算入门了,如果是 ios 开发,如果不会申请账号,不会搭建虚拟机,不会与苹果审核人员斗智斗勇,光学会几个 objectc 语法那是无法独立完成工作的. 而网络编程的入门标志是你能用程序向服务器发送命令了并且能收到回应.在我们的"那个年代",delphi 非常流行,有种叫控件的东西,可以直接完成很多工作,而不用太关心具体实现,所以当年我还没学会网络编程的时候就会写 ftp 文件传输程序了 ... 但是这是没有用的,因为有一天同学拿着我的软件向我反映说在学校机房用不了,我百思不得其解,把代码检查了一遍又一遍,直到很多年后我才明白真正的原因(是 port 命令的问题,具体说就太远了).

当然了我并不是否定控件,我们现在写 php,java 一般情况下也不需要自己去实现 smtp 这样的过程,我只是说那样的话你算不得是入了网络编程的门(比如现在的 golang 默认的 smtp 模块,你不明白 smtp 原理的话就是用不起来的).

真正的网络编程入门应该这样思考,我用的这个控件或者是 php 模块是怎样实现发送邮件的呢? 当然看了前面的几篇文章大家知道是发送一条条命令来实现的,那么怎么在我的语言环境中发送一条命令呢? 解决了这个问题就算是入门了.

说来惭愧我是工作了近一年后才知道网络命令的发送最终是要使用到操作系统的 socket 函数的. windows 下,linux 下都是如此,包括现在的手机安卓,ios 也是如此,还有些大家不知道的环境下也是如此. 写网络应用程序是一定要学会 socket 编程的, "socket" 就是网络编程的关键字.所以要学会某个平台下的网络编程,例如 ios 的,那么只要在 baidu 上输入 "ios socket" 就可以了.不过这里还要先提醒一下大伙,手机平台下反而是不推荐直接使用原始的 socket 进行编程,要使用系统二次封装后的函数,原因我们后面再说,不过原理都是一样的,所以还是得先学会基础的 socket 编程.

说了这么多废话快让我们开始吧! 说到开始还真犯难,用什么环境来做示例呢,曾经有本 O'Reilly 的 email 编程书读者评论好的说它很好,差的说它很差,说它差的人主要就是批评它都是理论根本不能上手.我个人觉得它主要是用 perl 和 java 的已有模块来做示例,根本就不能从原理层次去进入尝试. 手机开发环境是要用地次封装的肯定不适合,java 很流行,但也是封装过的,而且我个人并不喜欢 java,foxmail 和我写的 eemail 都是 delphi 的,但我要用 delphi 的话估计现在的程序员没几个能看得懂的,那就只好走传统路线用 C 语言了. 但 C 语言的环境并不好处理,象 vc 的话就要加入 lib 等等,这些操作其实也是要脱一层皮的. 想来想去,我决定给出多种语言的一个最基本示例,大家下载后就可以直接用,而我后面用来讲解的则会使用我一个专门用来测试的 C 语言小环境,大家可以当伪码来看.

不论是哪个环境,大概的 socket 流程都是这样:

.初始化 socket 环境(windows 下必须有);
.将域名转换为 ip 地址(很多书里都不会介绍这个);
.连接上这个 ip 地址;
.发送一个字符串;
.接收一个字符串.

在传统中前三个步骤相当繁琐,特别是第二步很容易误导初学者,所以对于大多数环境来说,现在的底层网络开发环境实际上又封装过一点,变成了:

.连接上域名或者是 ip 地址;
.发送一个字符串;
.接收一个字符串.

我们把这三个过程用三个伪码函数表示为 connect(), send(), recv() 方便我们讨论,对于大多数情况下它们的参数都可以直接理解为字符串(string),只有某些情况下需要理解为二进制内存缓冲块(bytes),所以实际上从根本上来说一个平台只要实现这三个函数就可以完成所有的网络通讯工作了! 初学者还没什么,学过 socket 的同学一定会非常震惊,socket 函数可有很多很多的! 没错,但那些都不过是辅助或者性能更佳的替代品而已,象windows的完成端口,linux 的 epoll 是很难很有用,但它们完成的功能说到底不过是 send(), recv() 而已,我就封装过完成端口给一个公司用过. golang 的网络开发环境就是这样封装过的.

考虑再三,我决定先给出受众最广的 java 版本测试代码. C 语言版本当然最重要,后面再专门讨论.

先给出完整代码,不过大家先别急着细看.

package st;

import java.io.*;
import java.net.*; public class SocketTest1 { public static BufferedReader br = null;
public static PrintWriter pw = null;
public static Socket socket = null;
public static OutputStream os = null;
public static InputStream is = null; public static void main(String[] args) { try{ System.out.println("start"); //简单的测试一下 smtp
test_smtp(); br.close();
is.close();
pw.close();
os.close();
socket.close(); }catch(Exception e) {
System.out.println("Error."+e); } }// //简单的测试一下 smtp
public static void test_smtp()
{
//连接
Connect("newbt.net", 25); //收取一行
String line = RecvLine();
System.out.println("recv:" + line); //发送一个命令
SendLine("EHLO"); //收取一行
line = RecvLine();
System.out.println("recv:" + line);
} public static void Connect(String host, int port)
{
try{
//socket = new Socket("newbt.net", 25);
socket = new Socket(host, port); //--------------------------------------------------
os = socket.getOutputStream();//字节输出流
pw = new PrintWriter(os);//将输出流包装成打印流 is = socket.getInputStream();
br = new BufferedReader(new InputStreamReader(is)); }catch(Exception e) {
System.out.println("Error."+e); }
} //发送一个命令行
public static void SendLine(String s)
{
//pw.write("EHLO\r\n");
pw.write(s + "\r\n");
pw.flush();
} //收取一行服务器发来的信息
public static String RecvLine()
{
try{
String s = br.readLine();
return s;
}catch(Exception e) {
System.out.println("Error."+e); } return null;
} }//

代码也不算长,不过我仍然觉得啰嗦,既然是用来完成邮件发送收取工作的,其实只需要关心 test_smtp() 函数的内容就行了,完成前几篇文章,乃至整个邮件通讯过程用这其中的三个函数就行了!

真的!我一点都不夸张,不过真实的环境中还要设置网络超时等,不过这些都应该封装到 connect 函数或者其他地方,通讯逻辑上就是这几个函数就行了,就这几个函数就可以实现前面用 telnet 登录到发送邮件的整个过程,即以下代码:

    //简单的测试一下 smtp
public static void test_smtp()
{
//连接
Connect("newbt.net", 25); //收取一行
String line = RecvLine();
System.out.println("recv:" + line); //发送一个命令
SendLine("EHLO"); //收取一行
line = RecvLine();
System.out.println("recv:" + line);
}

一个 SendLine 就相当于您在 telnet 中的输入一行命令然后按下回车键这个动作,而 RecvLine 函数做的工作就是把命令的结果从服务器下取下来,取下来的结果再输出就是相当于 telnet 中的命令结果显示了.

需要说明的是 RecvLine 只收取一行,而 telnet 是有多少收多少,所以这里的示例其实是没有取完 "EHLO" 命令的全部结果的,要调用 RecvLine 取多少次那就要分析 smtp 协议了,这里可以先说一下,好让大家向下测试:

要接收到服务回应的行中有 "250" 但是没有 "250-" 为止,说来拗口,不过这是非常精确的表述,心急要先测试的同学们可以仔细思考.不急的同学跟我们向前走吧.

下一篇要说 C 语言的实现,会复杂很多.

--------------------------------------------------

注1:这里虽然是 java 代码,但是不能直接用在现在的安卓环境中,因为现在的安卓环境要求要放线程中,这是有原因的,我们后面再解释.

版权声明:
本系列文章已授权百家号 "clq的程序员学前班" .