java网路编程学习之路(2)

第二章  Socket用法详解

在客户端和服务器端进行通信时,客户端需要主动创建与服务器端连接的Socket,服务器端收到了客户端的连接请求后,也会创建一个与客户端连接的Socket。Socket可以看做是通信连接两端的收发器,服务器与客户端都通过Socket来收发数据。


2.1 构造Socket

Socket的构造方法见下图

除了第一个不带参数的构造方法以外,其他的方法都试图与服务器建立连接,如果连接成功则返回Socket对象;如果连接失败则抛出相应的异常。

我们可以利用Socket的构造方法来测试操作系统当前哪些端口号被占用,哪些没被占用。
import java.io.IOException;
import java.net.Socket;

public class PortScanner {
	public static void main(String[] args) {
		String host = "localhost";
		new PortScanner().scan(host);
	}
	public void scan(String host){
		Socket socket = null;
		for(int port = 1024;port < 1600;port++){//检测1024-1600端口
			
try {
				socket = new Socket(host, port);
				System.out.println("有一个服务器在"+port+"端口上运行");
			} catch (IOException e) {
				System.out.println("端口"+port+"正在被占用");
			}finally{
				if(socket!=null){
					try {
						socket.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}

}}}





2.1.1 设定等待建立连接的超时时间

当客户端请求与服务器端连接时,需要等待一段时间。默认情况下,会一直等下去,直到连接成功,或者出现异常。如果我们希望限定这个等待时间,可以通过以下代码来实现。
	Socket socket = new Socket();
	SocketAddress remoAddress = new InetSocketAddress("localhost", 8000);
	socket.connect(remoAddress, 60000);
以上代码用于连接本地的8000端口,如果1分钟内连接成功,则connect方法顺利返回;如果在1分钟内出现某些异常,则抛出该异常,如果连接时间超过1分钟,则既没有连接成功也没有抛出其他出错的异常,那么会抛出一个SocketTimeoutException,超时异常。Socket的connect(SocketAddress endpoint,int timeout)方法负责连接服务器,参数endpoint表示服务器地址,参数timeout表示设置的超时时间,单位为毫秒。

2.1.2 设定服务器的地址

我们都知道一台主机既有当前系统的主机名,又有IP地址作为地址的标识。一般情况下我们知道其中某一项就可以连接到服务器。
public Socket(InetAddress address,
              int port)
       throws IOException             
address - IP 地址。port - 端口号。
public Socket(String host,
              int port)
       throws UnknownHostException,
              IOException
host - 主机名,或者为 null,表示回送地址。
port - 端口号。 

InetAddress类表示服务器的IP地址,InetAddress类提供了一系列的静态工厂方法,用于构造自身的实例。
//返回本地主机的IP地址
InetAddress addr1 = InetAddress.getLocalHost();
//返回代表"222.34.5.7"的IP地址, 其实就是"222.34.5.7"
InetAddress addr2 = InetAddress.getByName("222.34.5.7");
//返回代表域名为"www.baidu.com"的IP地址
InetAddress addr3 = InetAddress.getByName("www.baidu.com"); //结果:www.baidu.com/220.181.111.148

2.1.3  设定客户端地址

在一个Socket对象中,既包含远程服务器的IP地址和端口信息,也包含本地客户端的IP地址和端口信息。默认情况下,客户端的IP地址来自于客户程序所在的主机,客户端的端口则由操作系统随机分配。上一篇日志中写的服务器端accept接受到的socket就包含客户端的信息,可以输出对应信息进行查看。
另外Socket类还有两个构造方法显示地设置客户端的IP地址和端口。

//参数localAddr和localPort用来设置客户端的IP地址和端口
详解上面Socket构造方法第4个和最后一个

如果一个主机同时属于两个以上的网络,它就有可能拥有两个以上的IP地址。例如,一个主机在访问外网时的IP地址是222.67.1.34,在局域网中的IP地址是112.5.4.3.假使这个主机的客户程序希望和同一个局域网上的一个服务器程序(112.5.4.45:8000)进行通信。客户端可以按照如下方式构造Socket对象:
InetAddress remoteAddr = InetAddress.getByName("112.5.4.45");
InetAddress localAddr = InetAddress.getByName("112.5.4.3");
Socket socket = new Socket(remoteAddr,8000,localAddr,6666);//客户端使用6666端口

2.1.4   客户端连接服务器时可能抛出的异常

● UnknowHostException:  无法识别主机的名字或IP地址
●ConnectException:没有服务器进程监听指定的端口,或者服务器进程拒绝连接

SocketTimeoutException:等待连接超时,上面有详细介绍
BindException:无法把Socket对象与指定的本地IP地址或端口绑定

以上4中通常都是IOException的直接或间接子类:

2.2  获取Socket的信息

关于Socket的相关信息,可以通过以下方法去获取

● getInetAddress();获得远程服务器的IP地址

● getPort();获得远程服务器的端口

● getLocalPort();获得客户端本地的端口

● getLocalAddress();获得客户本地的IP地址

● getInputStream();获得输入流

● getOutputStream();获得输出流

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class HTTPClient {
	String host = "www.baidu.com";
	int port = 80;
	Socket socket =null;
	public void createSocket()throws Exception{
		socket = new Socket(host,port);
	}
	
	public void communicate()throws Exception{
		String str ="GET /index.html HTTP/1.1\n"
				+"Accept: */*\n"
                + "Accept-Language: zh-cn;q=0.5\n"
                + "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) ; CIBA; .NET CLR 2.0.50727)\n"
                + "Host: www.baidu.com\n" + "Connection: Keep-Alive\n"
                + "\n"; 
		//上面都是HTTP的一些头部信息
		//发出HTTP请求
		OutputStream socketOut = socket.getOutputStream();
		socketOut.write(str.getBytes());
		socket.shutdownOutput();//关闭输出流
		
		//接受响应结果
		InputStream socketIn = socket.getInputStream();
		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
		byte[] buff = new byte[2048];
		int len = -1;
		while((len=socketIn.read(buff))!=-1){
			buffer.write(buff,0,len);
		}
		System.out.println(new String(buffer.toByteArray()));
		socket.close();
	}
	public static void main(String[] args) throws Exception {	
		HTTPClient client = new HTTPClient();
		client.createSocket();
		client.communicate();
	}
}
以上代码用于访问www.baidu.com/index.html网页,并且接受从HTTP服务器发回的相应结果。
如果访问页面发回的数据较多,则用BufferedReader来接受读取的数据并输出
        InputStream is = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(is,
                "utf-8"));
        String line = null;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }

2.3 关闭Socket

当客户端与服务器的通信结束时,应该及时关闭Socket,以释放相应的资源。Socket有个close()就是用来关闭Socket对象的。需要注意的是,当一个Socket对象被关闭时,就不能再对其进行相关流操作,否则会抛出异常。
为了确保关闭Socket的操作总是被执行,最后将其放到finally中

			try {
				socket = new Socket(host, port);
				System.out.println("有一个服务器在"+port+"端口上运行");
			} catch (IOException e) {
				System.out.println("端口"+port+"正在被占用");
			}finally{
				if(socket!=null){
					try {
						socket.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}


Socket类提供了3个状态检测的方法
● isClosed():如果Socket已经连接到远程主机,并且还没有关闭,则返回true,否则返回false。
● isConnected():如果Socket曾经连接到远程主机,则返回true,否则返回false。
●isBound():如果Socket已经与一个本地端口绑定,则返回true,否则返回false。
如果要判断一个Socket对象是否处于连接状态,可以采用以下方式:
boolean isConnected = socket.isConnected()&&!socked.isClosed();

2.4  半关闭Socket

进程A和进程B通过Socket通信,假设进程A输出数据,进程B读入数据。进程A如何告诉进程B数据已经输出完了呢?有以下几种方法可以使用:
(1).进程之间可以通过事先约定的一个特殊的字符串作为结束标志,如以字符串“bye”作为结束标志。在第一章的例子中用的就是这个方法。当进程B读到标志字符串时就停止读数据了。

(2).进程A先发一个消息,告诉进程B它一共要传给B的数据的长度是多少,当进程B读完该长度的内容后就停止读数据。

(3).进程A发完所有的数据后,关闭Socket。此时进程B再读入进程A发送的数据时read()方法会返回-1值,这时进程B就知道已经读完了。

(4).当调用Socket()的Close()方法时,往往将输出流和输入流都关闭了。有时候,我们只希望关闭其中之一。这时候,我们可以采用Socket类的半关闭方法。

● shutdownInput():关闭输入流。
● shutdownOutput():关闭输出流。

上面2.2中就用到了瓣关闭流
OutputStream socketOut = socket.getOutputStream();
socketOut.write(str.getBytes());
socket.shutdownOutput();//关闭输出流

进程B再读入数据时,如果进程A的输出流已经关闭,进程B读入所有数据后,就会读到输入流的末尾。
要注意的是,先后调用以上2个方法关闭输入输出流并不等同于调用了Socket的close()方法。所以通信结束后,为了释放占用的资源,仍需调用close()方法。

Socket类还提供了2个状态测试的方法,来判断输入流输出流是否关闭
● public boolean isInputShutdown();  如果关闭返回true,否则返回false

● public boolean isOutputShutdown(); 同上

当客户端与服务器端通信时,如果有一方突然关闭时会产生什么影响。我们来通过一个例子了解:

Sender.java
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;

public class Sender {
	private String host = "localhost";
	private int port = 8000;
	private Socket socket;
	private static int stopWay = 1;// 结束通信的方式
	private final int NATURL_STOP = 1;// 自然结束
	private final int SUDDEN_STOP = 2;// 突然终止程序
	private final int SOCKET_STOP = 3;// 关闭socket,再结束程序
	private final int OUTPUT_STOP = 4;// 关闭输出流,再结束程序

	public Sender() throws Exception {
		socket = new Socket(host, port);
	}

	public static void main(String[] args) throws Exception {
		stopWay = 2;
		new Sender().send();
	}

	private PrintWriter getWriter(Socket socket) throws Exception {
		OutputStream socketOut = socket.getOutputStream();
		return new PrintWriter(socketOut, true);// true表示自动将中间缓存flush到接受数据端
	}

	private void send() throws Exception {
		PrintWriter pw = getWriter(socket);
		for (int i = 0; i < 20; i++) {
			String msg = "hello:" + i;
			pw.println(msg);
			System.out.println("send:" + msg);
			Thread.sleep(500);
			if(i == 2){
				if(stopWay == SUDDEN_STOP){
					System.out.println("突然终止程序");
					System.exit(0);
				}else if(stopWay == SOCKET_STOP){
					System.out.println("关闭socket,再结束程序");
					System.exit(0);
				}else if(stopWay == OUTPUT_STOP){
					socket.shutdownOutput();
					System.out.println("关闭输出流,在结束程序");
					break;
				}
			}
		}
		if(stopWay == NATURL_STOP){
			socket.close();
		}
	}
}


Receiver.java
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Receiver {
	private int port = 8000;
	private ServerSocket serverSocket;
	private static int stopWay = 1;// 结束通信的方式
	private final int NATURL_STOP = 1;// 自然结束
	private final int SUDDEN_STOP = 2;// 突然终止程序
	private final int SOCKET_STOP = 3;// 关闭socket,再结束程序
	private final int INPUT_STOP = 4;// 关闭输入流,再结束程序
	private final int SERVERSOCKET_STOP = 5;// 关闭ServerSocket,再结束程序
	
	
	public Receiver() throws Exception{
		serverSocket = new ServerSocket(port);
		System.out.println("服务器已经启动");
	}
	
	private BufferedReader getReader(Socket socket) throws Exception{
		InputStream socketIn = socket.getInputStream();
		return new BufferedReader(new InputStreamReader(socketIn));
	}
	
	public void receive() throws Exception{
		Socket socket = null;
		socket = serverSocket.accept();
		BufferedReader br = getReader(socket);
		
		for(int i = 0;i < 20;i++){
			String msg = br.readLine();
			System.out.println("receive:"+msg);
			Thread.sleep(1000);
			if(i == 2){
				if(stopWay == SUDDEN_STOP){
					System.out.println("突然终止程序");
					System.exit(0);
				}else if(stopWay == SOCKET_STOP){
					System.out.println("关闭socket,再结束程序");
					System.exit(0);
				}else if(stopWay == INPUT_STOP){
					socket.shutdownInput();
					System.out.println("关闭输入流,在结束程序");
					break;
				}else if(stopWay == SERVERSOCKET_STOP){
					System.out.println("关闭ServerSocket,再结束程序");
					serverSocket.close();
					System.exit(0);
				}
			}
			
			if(stopWay == NATURL_STOP){
				socket.close();
				serverSocket.close();
			}
		}
	}
	public static void main(String[] args) throws Exception {
		stopWay = 3;
		new Receiver().receive();
	}
}
可以通过改变上面的topWay值来观察不同的结果。

1.自然结束Sender和Receiver通信
先运行Receiver,再运行Sender,Sender会发生20行字符串,然后自然结束运行,Receiver会接受20行字符串,然后也自然结束运行。
2.提前终止Receiver
设置Receiver里面的stopWay值为2,3,4,5并运行,然后再运行Sender,Receiver接受了3行字符串后,就结束运行。但是Sender仍会发生完20行字符串,才自然结束运行。因为,尽管Receiver已经结束运行,但底层的Socket并没有立即释放本地端口,操作系统探测到还有发送给该Socket的数据,会使底层ocket继续占用本地端口一段时间。后面2.5.2(SO_RESUSEADDR选项)会做进一步解释。
3突然中止Sender
先运行Receiver,再运行Sender,Sender发送了3行字符串后,在没有关闭Socket的情况下,就结束了运行。Receiver在第4次执行BufferedReader的readLine()方法时会抛出异常。java.net.SocketException:Connection reset
4.关闭或半关闭Sender的Socket
先运行Receiver,再运行Sender,设置Sender的stopWay=3或者4,。Sender发送了3行字符串后,会关闭Socket或者关闭Socket的输出流,然后结束运行。Receiver在第4次执行BufferedReader的readLine方法时读到输入流的末尾。

2.5 设置Socket的选项

Socket有以下几个选项:
● TCP_NODELAY:表示立即发送数据
● SO_RESUSEADDR:表示是否允许重用Socket所绑定的本地地址。
 SO_TIMEOUT:表示接受数据时的等待超时时间
 SO_LINGER:表示当执行Socket的close()方法时,是否立即关闭底层的Socket。
 SO_SNFBUF:表示发送数据的缓冲区的大小
 SO_RCVBUF:表示接受数据的缓冲区的大小
 SO_KEEPALIVE:表示对于长时间处于空闲状态的Socket,是否要自动把它关闭。
 OOBINLINE:表示是否支持发送一个字节的TCP紧急数据。

2.5.1 TCP_NODELAY选项

设置该选项:public void setTcpNoDelay(boolean on)throws SocketException
读取该选项:public boolean getTcpNodelay() throws SocketException

默认情况下,发送数据采用Negale算法。该算法是指发送方发送的数据不会立刻发出,而是先放在缓存区内,等待缓冲区慢了在发生。发送完一批数据后,会等待接受放对这批数据的回应,然后再发送下一批数据。Negale算法适用于发送方需要发送大批量数据,并且接收方会及时作出回应的场合,这种算法通过减少传输数据的次数来提高通信效率。
如果发送方是发送小批量数据并且接收方不会作出快速回应,则使用Negale算法会使发送方运行很慢。对于GUI程序,如网络游戏程序,服务器端需要实时跟踪客户端鼠标的移动,采用Negale算法会采用缓冲,所以会降低实时响应速度,所以该算法在这里就不适合用了。
TCP_NODELAY的默认值是false,表示采用Negale算法。如果调用setTcpNoDelay(true)方法,就会关闭Socket缓冲,确保数据及时发送:
if(!socket.getTcpNodelay())socket.setTcpNoDelay(true);
 如果Socket的底层不支持该选项,那么使用那两个方法会抛出异常。

2.5.2 SO_RESUSEADDR选项

 设置该选项:public void setResuseAddress(boolean on)throws SocketException
   读取该选项:public boolean getResuseAddress() throws SocketException

当接收方通过Socket的close方法关闭Socket时,如果网络上海有发送到这个Socket的数据,那么底层的Socket不会立刻释放本地端口,而是会等待一段时间,确保接收到网络上发送过来的延迟数据,然后再释放端口。Socket接收到延迟数据后,不会对这些数据作出任何处理。Socket接收延迟数据的目的是确保这些数据不会被其他碰巧绑定到同样端口的进程收到。
客户程序一般采用随机端口,因此出现两个客户程序绑定到同样端口的可能性不大。许多服务器程序都使用固定的端口。当服务器程序关闭后,有可能它的端口还会被占用一段时间,如果此时立刻在同一个主机上重启服务器程序,由于端口已经被占用,导致启动失败。
为了确保一个进程关闭Socket后,即使它还没有释放端口,同一个主机上的其他进程还可以立刻重用该端口,可以调用Socet的setResuseAddress(true)方法
if(!socket.getResuseAddress())socket.setResuseAddress(true);
值得注意的是socket.setResuseAddress(true)必须在Socket还没有绑定到一个本地端口之前调用,否则该方法不起作用
		Socket socket = new Socket(); //此时Socket对象未绑定本地端口,并且未连接远程服务器
		socket.setReuseAddress(true);
		SocketAddress remoteAddr = new InetSocketAddress("remotehost",8000);
		socket.connect(remoteAddr);//连接远程服务器,并且绑定匿名的本地端口
//		或者
//		Socket socket = new Socket(); //此时Socket对象未绑定本地端口,并且未连接远程服务器
//		socket.setReuseAddress(true);
//		SocketAddress localAddr = new InetSocketAddress("localhost",9000);
//		SocketAddress remoteAddr = new InetSocketAddress("remotehost",8000);
//		socket.bind(localAddr);//与本地端口绑定
//		socket.connect(remoteAddr);//连接远程服务器
此外,两个共用同一个端口的进程必须都调用setResuseAddress(true)方法,才能达到一个进程关闭Socket后,即使它还没有释放端口,同一个主机上的其他进程还可以立刻重用该端口这种效果。

2.5.3 SO_TIMEOUT选项

 设置该选项:public void setSoTimeout(int milliseconds)throws SocketException
   读取该选项:public int getSoTimeout() throws SocketException

Socket类的SO_TIMEOUT选项用于设定接受数据的等待超时时间,单位毫秒,它的默认值为0表示会无限等待,永不会超市。以下代码表示把接受数据的超时时间设置为3分钟
if(!socket.getTimeout()==0)socket.setTimeout(60000*3);
注意Socket的setTimeout方法必须在接受数据之前调用才有效。

2.5.4 SO_LINGER选项

 ●设置该选项:public void setSoLinger(boolean on,int seconds) throws SocketException //注意这里时间单位是秒
 ●  读取该选项:public boolean getSoLinger() throws SocketException

SO_LINGER选项用来控制Socket关闭时的行为。默认情况下,执行Socket的close方法,该方法会立即返回,但底层的Socket实际上并不立即关闭,它会延迟一段时间知道发送完所有的数据。
如果执行以下方法:
socket.setSoLinger(true,0);
那么执行Socket的close方法时,该方法会立即返回并且底层的Socket会立即关闭,未发送完的数据会被丢弃。
如果执行以下方法
socket.setSoLinger(true,3600);
那么执行Socket的close方法时,该方法不会立即返回,而是进入了阻塞状态。同时,底层Socket正在发生未发送完的数据。只有满足以下两个条件之一,close方法才会返回.
(1).底层的socket已经发送完了所有的剩余数据;
(2).阻塞时间到了3600秒,也会返回,未发送完的数据会被丢弃。
值得一提的是,在以上两种情况内,close返回后,底层Socket会被关闭,断开连接。

2.5.5 SO_RCVBUF选项

 ●设置该选项:public void setReceiveBufferSize(int size) throws SocketException
 ●  读取该选项:public int getReceiveBufferSize() throws SocketException

 SO_RCVBUF表示Socket的用于输入数据的缓冲区的大小。一般来说,传输大的数据块(基于HTTP或FTP协议的通信)可以使用较大的缓冲区,以便于减少传输数据的次数,提高传输小姑。而对于交互频繁且单次传输数据较小的(例如网路游戏)可以用较小的缓冲区,以便于确保小批量数据及时传送给对方,降低延迟。
如果底层Socket不支持该选项,那么在调用以上两个方法时会抛出异常。

2.5.6 SO_SNDBUF选项

 ●设置该选项:public void setSendBufferSize(int size) throws SocketException
 ●  读取该选项:public int getSendBufferSize() throws SocketException

SO_SNFBUF表示Socket的用于输出数据的缓冲区的大小。缓冲区大小选定方法跟SO_RCVBUF类似。
如果底层Socket不支持该选项,那么在调用以上两个方法时会抛出异常。

2.5.7 SO_KEEPALIVE选项

 ●设置该选项:public void setKeepAlive(boolean on) throws SocketException
 ●  读取该选项:public int getKeepAlive() throws SocketException

当该选项为true时,表示底层的TCP实现会监视连接是否有效。当连接处于空闲时间超过2小时时,本地的TCP会发送一个数据包给远程的Socket。如果远程Socket没有及时发回响应,TCP实现就会持续尝试11分钟,直到接收到响应为止。如果再等到了12分钟还未收到响应,那么TCP实现就会自动关闭本地Socket,断开连接。在不同的网络平台上,TCP实现尝试与远程Socket对象的时限会有所差别。
该选项的默认值是false,即不会进行上述监视操作,不活动的客户端会永久存在,甚至不会注意到服务器已经崩溃。
if(!socket.getKeepAlive()) socket.setKeepAlive(true); //设置该选项值为true

2.5.8 OOBINLINE选项

 ●设置该选项:public void setOOBInline(boolean on) throws SocketException
 ●  读取该选项:public int getOOBInline() throws SocketException
当该选项值为true时,表示支持发送一个字节的TCP紧急数据。Socket类的sendUrgentDate(int data )方法用于发生一个字节的TCP紧急数据。
该选项默认值为false,即如果接收方收到紧急数据会将其废弃。
socket.setOOBInline(true); //设置该选项值为true
此时接收方会将紧急数据与普通数据放在同样队列中,除非使用一些更高层次的协议,否则接收方很难区分并处理特殊紧急数据。

2.5.9 服务器类型选项

IP规定了4种服务器的类型,用来描述服务的质量
●低成本:发送成本低。
●高可靠性:保证把数据可靠地送达目的地。
最高吞吐量:一次可以接收或发送大批量的数据。
最小延迟:传输数据的速度快,把数据快速送达目的地。
这4种服务类型可以进行组合使用,例如,获取最高可靠性和最小延迟的。
Socket类中使用服务类型的方法如下:

●设置服务类型: public void setTrafficClass(int trafficClass) throws SocketException;
●读取服务类型: public int getTrafficClass() throws SocketException;

Socket类用4个整数(十六进制)表示服务类型
●低成本:0x02 (转换为二进制的倒数第二位为1)
●高可靠性:0x04(倒数第三位为1)
最高吞吐量:0x08(倒数第四位为1)
最小延迟:0x10(倒数第五位为1)

例如,以下代码请求高可靠性传输服务:
	Socket socket = new Socket("www.baidu.com",80);
	socket.setTrafficClass(0x04);
如果要请求多项服务参数用或运算,转换成2进制再进行或运算
	Socket socket = new Socket("www.baidu.com",80);
	socket.setTrafficClass(0x04|0x10);//或的结果是倒数第三五位都是1

2.5.10 设定连接时间、 延迟和带宽的相对重要性

在JDK1.5中,为Socket类提供了一个setPerformancePreferences()方法:
public void setPerformancePreferences(int connectionTime,int latency,int bandwidth)

以上方法三个参数表示网络传输数据的3项指标:
● 参数connectionTime :表示用最少时间建立连接
● 参数latency:表示最小延迟
● 参数bandwidth:表示最高带宽

用法为,这3个参数值可以取任意整数,整数的大小决定他们之间的权重。例如
setPerformancePreferences(1,2,3);表示带宽最重要,其次是最小延迟,最后为最少连接时间。

2.6 发送邮件的SMTP客户程序

第一章有介绍SMTP协议是简单邮件传输协议。SMTP协议规定了把邮件从一方发送到另一方的规则。

主要的SMTP命令
SMTP命令说明
HELO/EHLO指明邮件发送者的主机地址
MAIL FROM指明邮件发送者的邮件地址
PCRT TO指明邮件接收者的邮件地址
DATA表示接下来要发送的邮件内容
QUIT结束通信
HELP查询服务器支持的命令

主要的SMTP应答码
应答码 说明
214帮助信息
220服务就绪
221服务关闭
250邮件操作完成
354开始输入邮件内容,以"."结束
421服务未就绪,关闭传输通道
501命令参数格式错误
502命令不支持
503错误的命令序列
504命令参数不支持


下面给一个SMTP客户程序与SMTP服务一次会话过程的例子。SMTP服务器程序所在的主机名称叫smtp.mydomain.com,服务器的响应数据(以"Server>开头")客户端的发送数据(以"Client>开头")

Server>220 smtp.mydomain.com SMTP service ready
Client>HELO ANGEL
Server>250 smtp.mydomain.com Hello ANGEL,pleased to meet you.
Client>MAIL FROM:<tom@abc.com>
Server>250 sender<tom@abc.com> OK
Client>RCPT TO:<linda@def.com>
Server>250 recipient<linda@def.com> OK
Client> DATA
Server>354 Enter mail,end with "." on a line by itself
Client> Subject:hello from haha
hi,I miss you very much
Client>.
Server>250 message sent
Client>QUIT
Server>221 goodbye

以上是一次SMTP的流程演示,我在windows下用telnet不知道为什么不行,在linux下用telnet应该可以,下面用java代码来实现一个邮件发送功能

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

import sun.misc.BASE64Encoder;
public class MailSender {
	private String smtpServer = "smtp.163.com";//SMTP邮件服务器的主机名称
	private int port = 25;
	
	public static void main(String[] args) {
		Message msg = new Message("这里写你的163邮箱", "所要发送的目的邮箱", "hello", "hi,I miss you very much.");
		new MailSender().sendMail(msg);
	}
	private PrintWriter getWriter(Socket socket) throws IOException{
		OutputStream socketOut = socket.getOutputStream();
		return new PrintWriter(socketOut,true);//true表示自动将中间缓存flush到接受数据端
	}
	
	private BufferedReader getReader(Socket socket) throws IOException{
		InputStream socketIn = socket.getInputStream();
		return new BufferedReader(new InputStreamReader(socketIn));
	}
	public void sendMail(Message msg){
		Socket socket = null;
		
		try {
			socket = new Socket(smtpServer,port); //连接到邮件服务器
			BufferedReader br = getReader(socket);
			PrintWriter pw = getWriter(socket);
			String localhost = InetAddress.getLocalHost().getHostName();  //客户主机的名字
			String username = "";   //写你的163邮箱账户     
			String password = "";    //写你的密码
			//对用户名和密码进行Base64编码
			username = new BASE64Encoder().encode(username.getBytes());
			password = new BASE64Encoder().encode(password.getBytes());
			sendAndReceive(null,br,pw);  //仅仅是为了接收服务器的响应数据
			sendAndReceive("HELO "+localhost, br, pw);
			sendAndReceive("AUTH LOGIN", br, pw); //认证命令
			sendAndReceive(username, br, pw);  //用户名
			sendAndReceive(password, br, pw);  //密码
			sendAndReceive("MAIL FROM:<"+msg.from+">", br, pw);
			sendAndReceive("RCPT TO:<"+msg.to+">", br, pw);
			sendAndReceive("DATA", br, pw);       //接下来开始发送邮件内容
			pw.println(msg.data);
			System.out.println("Client>"+msg.data);
			sendAndReceive(".", br, pw); //邮件发送完毕
			sendAndReceive("QUIT", br, pw);
		} catch (IOException e) {
			e.printStackTrace();
		} finally{
			if(socket!=null){
				try {
					socket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		
	}
	/**发送一行字符串,并且接受一行服务器的响应数据
	 * @throws IOException */
	private void sendAndReceive(String str,BufferedReader br,PrintWriter pw) throws IOException {
		if(str != null){
			System.out.println("Client>"+str);
			pw.println(str);       //发送完str字符串后,还会发送"\r\n"
		}
		String response;
		if((response = br.readLine())!=null){
			System.out.println("Server>"+response);
		}
	}
}


class Message{
	String from;  //发送者的邮件地址
	String to;   //接收者的邮件地址
	String subject;  //邮件标题
	String content;  //邮件正文
	String data;    //邮件内容,包括邮件标题和正文
	public Message(String from,String to,String subject,String content){
		this.from = from;
		this.to = to;
		this.subject = subject;
		this.content = content;
		data = "Subject:"+subject+"\n\r"+content; //注意这里是\n\r,\r\n不行不知道为什么
	}
}

注意以上的BASE64加密那个类可能用不了,因为那个包总导不进去,如果你是用myeclipse或是eclipse编辑该项目,你只需要右击jre system library,buildpath将jre system library包给remove掉然后再右击项目buildpath重新加进来就可以了。

最后运行结果
Server>220 163.com Anti-spam GT for Coremail System (163com[20121016])
Client>HELO linux_v-PC
Server>250 OK
Client>AUTH LOGIN
Server>334 dXNlcm5hbWU6
Client>
Server>334 UGFzc3dvcmQ6
Client>
Server>235 Authentication successful
Client>MAIL FROM:<>
Server>250 Mail OK
Client>RCPT TO:<>
Server>250 Mail OK
Client>DATA
Server>354 End data with <CR><LF>.<CR><LF>
Client>Subject:hello

hi,I miss you very much.
Client>.
Server>250 Mail OK queued as smtp11,D8CowED5+l7WI01TXkN6AQ--.890S2 1397564374
Client>QUIT




如果\n\r不加在Subject和content中间的话就会默认全为标题了。\n\r是标题和内容的分界线。
本来smtp服务器想用QQ的,可是需要SSL认证什么的,所以弄不了,改用163的了。
是不是有点意思,赶快去试试吧,运行一次发送一次,打成jar程序,无限刷别人的邮箱有木有。。


2.7 总结

主要学习了Socket的详细用法,其中包含发送数据效率之类的等等。这一章学习了SMTP协议的大致内容,并且通过SMTP协议可以用java程序去发送邮件。
欢迎大家评论交流,共同学习。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值