核心技术Ⅱ:网络

来自Wikioe
跳到导航 跳到搜索


实现服务端

手动实现:telnet

telnet 是一种用于网络编程的调试工具,可以在命令shell中输入telnet来启动它。

  • 在Windows中,需要激活telnet:“控制面板”->“程序”->“打开/关闭Windows特性”->“Telnet客户端”。

如:

telnet time-a.nist.gov 13 
# 用time-a.nist.gov在端口13上建立telnet会话

连接到服务器端口的客户端.png

Java实现:socket

import java.io.*;
import java.net.*;
import java.util.*;
public class SocketTest lS {
public static void main(String[) args) throwsIOException 
{ 
	try(Socket s = new Socket(time-a.nist.gov,13); 
		Scanner in = new Scanner(s.getInputStream(),UTF-8'’))
	{
		while (in.hasNextline())
		{
			String line = in.nextline();
			System.out.println(line);
		}
	}
}

如果连接失败,它将抛出一个UnknownHostException异常;如果存在其他问题,它将抛出一个IOException异常。
网络基础

相关方法

jva.net.Socket 1.0

  • Socket(String host, int port)
    构建一个套接字,用来连接给定的主机和端口。
  • InputStream getlnputStream()
  • OutputStream getOutputStream()
    获取可以从套接字中读取数据的流,以及可以向套接字写出数据的流。

套接字超时

从套接字读取信息时,在有数据可供访问之前,读操作将会被阻塞。对于不同的应用,应该确定合理的超时值。

  1. 调用“setSoTimeout”方法设置超时值(单位:毫秒):
    Socket s = new Socket(. . .); 
    s.setSoTimeout(lOOOO); 
    // time out after 10 seconds
    
  2. 捕获“SocketTimeoutException”异常:
    如果已经为套接字设置了超时值,并且之后的读操作和写操作在没有完成之前就超过了时间限制,那么这些操作就会抛出SocketTimeoutException异常;
    try
    {
       InputStream in = s.getlnputStream();   //read from in 
       . . .
    }
    catch(InterruptedIOException exception)
    {
       . . .
    }
    
  3. 使用可以超时的无连接套接字:
    • “Socket s = new Socket(String host, int port);”会一直无限期地阻塞下去,直到建立了到达主机的初始连接为止;
    Socket s = new Socket(); 
    s.connect(new InetSocketAddress(host, port), timeout);
    

相关方法

jva.net.Socket 1.0

  • Socket() 1.1
    创建一个还未被连接的套接字。
  • void connect(SocketAddress address) 1.4
    将该套接字连接到给定的地址。
  • void connect(SocketAddress address, int timeoutlnMi11iseconds) 1.4
    将套接字连接到给定的地址。如果在给定的时间内没有响应,则返回。
  • void setSoTimeout(int timeoutlnMilliseconds) 1.1
    设置该套接字上读请求的阻塞时间。如果超出给定时间,则抛出一个InterruptedIOException异常。
  • boolean isConnected() 1.4
    如果该套接字已被连接,则返回true。
  • boolean isClosed() 1.4
    如果套接宇已经被关闭,则返回true

因特网地址

如果需要在主机名和因特网地址之间进行转换,那么就可以使用“InetAddress”类:

  • 只要主机操作系统支持1Pv6格式的因特网地址,java.net包也将支持它;
  1. 静态的“getByName”方法可以返回代表某个主机的InetAddress对象:
    然后,可以使用“getAddress”方法来访问这些字节;
    InetAddress address = InetAddress.getByName("time-a.nist.gov");
    byte[] addressButes = address.getAddress();
    
  2. 调用“getAllByName”方法来获得所有主机:
    (一些访问量较大的主机名通常会对应于多个因特网地址,以实现负载均衡)
    InetAddress[] addresses= InetAddress.getA11ByName(host) ;
    
  3. 使用静态的“getlocalHost”方法来得到本地主机的地址:
    InetAddress address = InetAddress.getLocalHost();
    

相关方法

jva.net.InetAddress 1.0

  • static InetAddress getByName( String host)
  • static InetAddress[] getAllByName(String host)
    为给定的主机名创建一个InetAddress对象,或者一个包含了该主机名所对应的所有因特网地址的数组。
  • static InetAddress getlocalHost()
    为本地主机创建一个InetAddress对象。
  • byte[] getAddress()
    返回一个包含数字型地址的字节数组。
  • String getHostAddress()
    返回一个由十进制数组成的字符串,各数字间用圆点符号隔开,例如,“129.6.15.28”。
  • String getHostName()
    返回主机名。

实现服务器

服务器套接字

  1. “ServerSocket”类用于建立套接字:
    ServerSocket s = new ServerSocket(8189);
    
  2. 建立一个负责监控端口8189的服务器:
    Socket incoming = s.accept();
    
  3. 到输入流和输出流:
    InputStream inStream = incoming.getinputStream();
    OutputStream outStream = incoming.getOutputStream();
    
  4. 转换成扫描器和写入器:
    Scanner in= new Scanner(inStream, "UTF-8"); 
    PrintWriterout= new PrintWriter(newOutputStreamWriter(outStream, "UTF-8"), true) ;
    
  5. 客户端发送信息:
    out.print1n("Hello! Enter BYE to exit.")
    

相关方法

java.net.ServerSocket 1.0

  • ServerSocket(int port)
    创建一个监昕端口的服务器套接字。
  • Socket accept()
    等待连接。该方法阻塞(即,使之空闲)当前线程直到建立连接为止。该方法返回一个Socket对象,程序可以通过这个对象与连接中的客户端进行通信。
  • void close()
    关闭服务器套接字。

为多个客户端服务

运用线程:每当程序建立一个新的套接字连接,也就是说当调用“accept()”时,启动一个新的线程来处理服务器和该客户端之间的连接,而主程序将立即返回并等待下一个连接。

  • 这种方法并不能满足高性能服务器的需求。为使服务器实现更高的吞吐量,可以使用“java.nio”包中一些特性。
package threadad;

import java.io.*;
import java.net.*;
import java.util.*;

pub1ic c1ass ThreadedEchoServe
{
	pub1ic static void main (String[] args )
	{
		try(ServerSocket s = new ServerSocket(8189))
		{
			int i = 1;
			while (true)
			{
				Socket incoming = s.accept();
				System.out.println("Spawning " + i) ;
				Runnab1e r = new ThreadedEchoHandler(incoming);
				Thread t = new Thread(r)
				t.start();
				i++;
			}
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
	}
}

class ThreadedEchoHandler implements Runnab1e
{
	privete Socket incoming;

	public ThreadedEchoHandler(Socket incomingSocket)
	{
		incoming = incomingSocket;
	}

	public void run()
	{
		try(InputStream inStream = incoming.getInputStream();
			OnputStream onStream = incoming.getOnputStream())
		{
			Scanner in new Scanner(inStream, "UTF-8");
			PrintWriter out new PrintWriter(
				new OutputStreamWriter(outStream, "UTF-8"),
				true);
			out.println("Hello! Enter BYE to exit.");
		}
		
		boolean done= false;
		while(!done && in.hasNextLine())
		{
			String line = in.nextline();
			out.println("Echo: "+ line);
			if (1ine.trim().equals("BYE"))
				done = true;
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
	}
}

半关闭

半关闭(half-close)提供了这样一种能力:套接字连接的一端可以终止其输出,同时仍旧可以接收来自另一端的数据。

客户端使用半关闭方法:

try(ServerSocket s = new ServerSocket(host, port))
{
   Scanner in = new Scanner(socket.getInputStream(), UTF-8);
   PrintWriter writer = new PrintWriter(socket.getOutputStream());
   
   // send request data 
   Writer.print( . . . ); 
   Writer.flush(); 
   socket.shutdownOutput(); 
   
   // now socket is ha1f-c1osed 
   // read response data 
   while(in.hasNextLine() != null) 
   {
      String line = in.nextLine(); 
      . . . 
   }
}

服务器端将读取输入信息,直至到达输入流的结尾,然后它再发送响应。

  • 该协议只适用于一站式(one-shot)的服务,例如HTTP服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接。

相关方法

java.net.Socket 1.0

  • void shutdownOutput() 1.3
    将输出流设为“流结束”。
  • void shutdownInput() 1.3
    将输入流设为“流结束”。
  • boo1ean isOutputShutdown() 1.4
    如果输出已被关闭,则返回true。
  • boo1ean isInputShutdown() 1.4
    如果输入已被关闭,则返回true。

可中断套接字

当连接到一个套接字时,当前线和将会被阻塞且到建立连接或产生超时为止。同样地,当通过套接字读写数据时,当前线程也会被阻塞直到操作成功或产生超时为止。

  • 当线程因套接字无法响应而发生阻塞时, 则无法通过调用interrupt来解除阻塞。

为了中断套接字操作,可以使用java.nio包提供的一个特性——“SocketChannel”类:

  1. 打开SocketChannel:
    通道(channel)并没有与之相关联的流,它所拥有的read和write方法都是通过使用Buffer对象来实现的(ReadableByteChannel接口和WritableByteChannel接口都声明了这两个方法)。
    SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));􀀀
    
    1. 如果不想处理缓冲区, 可以使用Scanner类从SocketChannel中读取信息:
      Scanner in = new Scanner(channel, "UTF-8");􀀀
      
    2. 通过调用静态方法“Channels.newOutputStream”可以将通道转换成输出流:
      OutputStream outStream = Channels.newOutputStream(channel);
      

相关方法

java.net.InetSocketAddress 1.4

  • InetSocketAddress(String hostname, int port)
    用给定的主机和端口参数创建一个地址对象,并在创建过程中解析主机名。如果主机名不能被解析,那么该地址对象的unresolved 属性将被设为true。
  • boolean isUnresolved()
    如果不能解析该地址对象, 则返回true。

java.nio.channels.SocketChannel 1.4

  • static SocketChannel open(SocketAddress address)
    打开一个套接字通道,并将其连接到远程地址。

java.nio.channels.Channels 1.4

  • static InputStream newinputStream(ReadableByteChannel channel)
    创建一个输入流,用以从指定的通道读取数据
  • static OutputStream newOutputStream(WritableByteChannel channel)
    创建一个输出流, 用以向指定的通道写入数据

获取Web 数

为了在Java程序中访问Web服务器,在更高的级别上进行处理,而不只是创建套接字连接和发送HTTP诸求。

URL 和 URI

java.net包对统一资源定位符(Uniform Resource Locator, URL)和统一资源标识符(Uniform Resource Identifier, URI)作了非常行用的区分:

  1. URI是个纯粹的语法结构,包含用来指定Web资源的字符串的各种组成部分;
  2. URL是URI的一个特例,它包含了用于定位Web资源的足够信息;
  3. URN (uniform resource name, 统一资源名称),不属于定位符,因为根据该标识符我们无法定位任何数据;


URL句法:

[scheme:]schemeSpecificPart[#fragment]
  • “[...]”表示可选部分,并且“:”和“#”可以被包含在标识符内;
  1. 包含“scheme:”部分的URI称为“绝对URI”。否则,称为“相对URI”。
  2. 如果绝对URI的schemeSpecificPart不是以“/”开头的,就称它是“不透明的URI”。如:
    mailto:cay@horstmann.com
    
  3. 所有绝对的透明URI和所有相对URI都是分层的(hierarchical)。 如:
    http://horstmann.com/index.html
    ../../java/net/Socket.html#Socket()
    
  4. 一个分层URl的schemeSpecificPart具有以下结构:
    [//authority][path][?query]􀀁
    
    • “[. . .]”同样表示可选的部分。
  5. 对于那些基于服务器的URI,authority 部分具有以下形式:
    [user-info@]host[:port]
    


URI类,作用:

  1. 解析标识符并将它分解成各种不同的组成部分:
    getScheme􀀁
    getSchemeSpecificPart􀀁
    getAuthority􀀁
    getUserlnfo􀀁
    getHost􀀁
    getPort􀀁
    getPath􀀁
    getQuery􀀁
    getFragment􀀁
    
  2. 处理绝对标识符和相对标识符:
    combined = base.reso1ve(relative);   // 解析相对URL
    relative = base.relativize(combined);   // 相对化URL
    
    1. 解析相对URL:
      示例
      1绝对URI
      http://docs.mycompany.com/api/java/net/ServerSocket.html
      2相对URI
      ../../java/net/Socket.html#Socket()
      
      由12 组合出一个绝对URI
      http://docs.mycompany.com/api/java/net/Socket.html#Socket()
      
    2. 相对化(relativization):
      示例
      1URI
      http://docs.mycompany.com/api􀀁
      2URI
      http://docs.mycompany.com/api/java/1ang/String.html􀀁
      
      相对化之后的URI
      java/1ang/String.htm1􀀁
      

使用URLConnection 获取信息

使用“URLConnection”类,通过它得到比基本的URL类更多的控制功能。
操作步骤:

  1. 调用URL类中的“openConnection”方法获得“URLConnection”对象:
    URLConnection connection = url.openConnection();
    
  2. 使用以下方法来设置任意的请求属性:
    1. “setDoInput”:
    2. “setDoOutput”:
    3. “setIfModifiedSince”:􀀁
    4. “setUseCaches”:
    5. “setAl1owUserInteraction”:
    6. “setRequestProperty”:
    7. “setConnectTimeout”:
    8. “setReadTimeout”:
  3. 调用connect方法连接远程资源:
    connection.connect();
    
    • 除了与服务器建立套接字连接外, 该方法还可用于向服务器查询头信息(headerinformation)。
  4. 与服务器建立连接后,可以查询头信息。
    1. “getHeaderFieldKey”和“getHeaderField”这两个方法枚举了消息头的所有字段。
    2. “getHeaderFields”方法返回一个包含了消息头中所有字段的标准Map对象。
    以下方法可以查询各标准字段:
    1. “getContentType”:
    2. “getContentlength”:
    3. “getContentEncoding”:
    4. “getDate”:
    5. “getExpiration”:
    6. “getLastModified”:
  5. 最后,访问资源数据。
    使用“getInputStream”方法获取一个输入流用以读取信息(这个输人流与URL类中的“openStream”方法所返回的流相同)。
    (另一个方法getContent在实际操作中并不是很有用)
    • 由标准内容类型(比如“text/plain”和“image/gif”) 所返回的对象需要使用“com.sun”层次结构中的类来进行处理。

相关方法

java.net.URL 1.0

  • InputStream openStream()
    打开一个用于读取资源数据的输入流。
  • URLConnection openConnection();
    返回一个URLConnection对象, 该对象负责管理与资源之间的连接。

java.net.URLConnection 1.0

  • void setDolnput(boolean doInput)
  • boolean getDoInput()
    如果doInput为true, 那么用户可以接收来自该URLConnection的输入。
  • void setDoOutput(boolean doOutput)
  • boolean getDoOutput()
    如果doOutput为true, 那么用户可以将输出发送到该URLConnection
  • void setIfModifiedSince(long time)
  • long getIfModifiedSince()
    属性ifModifiedSince用于配置该URLConnection对象,使它只获取那些自从某个给定时间以来被修改过的数据。调用方法时需要传入的time 参数指的是从格林尼治时间1970年1月1日午夜开始计葬的秒数。
  • void setUseCaches(boolean useCaches)
  • boolean getUseCaches()
    如果useCaches为true, 那么数据可以从本地缓存中得到。 诮注意,URLConnection本身并不维护这样一个缓存, 缓存必须由浏览片眨之类的外部程序提供。
  • void setAllowUserlnteraction(boolean allowUserinteraction)
  • boolean getAl1owUserinteraction()
    如果allowUserlnteraction为true, 那么可以查询用户的口令。 请注意,URLConnection本身并不提供这种查询功能。 查询必须由浏览器或浏览器插件之类的外部程序实现。 void setConnectTimeout(int timeout) 5.0
  • int getConnectTimeout() 5. 0
    设置或得到连接超时时限(单位:毫秒)。 如果在连接建立之前就已经达到了超时的时限,那么相关联的输入流的connect方法就会抛出一个SocketTimeoutException异常。 void setReadTi meout(int ti me out) 5. O
  • int getReadTimeout () 5. 0
    设笠读取数据的超时时限(单位:亳秒)。 如果在一个读操作成功之前就已经达到了超时的时限, 那么read 方法就会抛出一个 SocketTi meoutExcept ion异常。
  • void setRequestProperty(String key, String value)
    设置请求头的一个字段。
  • Map<String, List<String>> getRequestProperties() 1. 4
    返回诸求头屈性的一个映射表。 相同的键对应的所有值被放嚣在同一个列表中。
  • void connect()
    连接远程资源并获取响应头信息。
  • Map<String, List<String>> getHeaderFields() 1. 4
    返回响应的一个映射表。 相同的键对应的所有值被放翌在同一个列表中。
  • String getHeaderFieldKey(int n)
    得到响应头第n个字段的键。 如果n 小于等于0或大于响应头字段的总数, 则该方法返回null值。
  • String getHeaderField(int n)
    得到响应头第n个字段的值。 如果n 小于等于0或大于响应头字段的总数, 则该方法返回null值。
  • int getContentlength()
    如果内容长度可获得, 则返回该长度值, 否则返回-1。
  • String getContentType()
    获取内容的类型 , 比如text/plain 或 image/gif。
  • String getContentEncoding()
    获取内容的编码机制,比如gzip。这个值不太常用,因为默认的identity 编码机制并不是用Content-Encoding头来设定的。
  • long getDate()
  • long getExpiration()
  • long getlastModified()
    获取创建日期 、 过期日以及最后一次被修改的日期。 这些日期指的是从格林尼治时间1970年1月1日午夜开始计算的秒数。
  • InputStream getInputStream()
  • OutputStream getOutputStream()
    返回从资源读取信息或向资源写入信息的流。
  • Object getContent()
    选择适当的内容处理器,以便读取资源数据并将它转换成对象。该方法对于读取诸如“text/plain”或“image/gif”之类的标准内容类型并没有什么用处,除非你安装了自己的内容处理器。

提交表单数据

有许多技术可以让Web服务器实现对程序的调用。 其中最广人所知的是Java Servlet、 JavaServer Face、 微软的ASP (Active Server Pages, 动态服务器主页)以及CGI (Common Gateway Interface, 通用网关接口)脚本。

执行服务器端脚本过程中的数据流.png

当表单数据被发送到Web服务器时,数据到底由谁来解释并不重要,可能是Servlet或CGI脚本,也可能是其他服务器端技术。客户端以标准格式将数据发送给Web服务器,而Web服务器则负责将数据传递给具体的程序以产生响应。

在向Web服务器发送信息时, 通常有两个命令会被用到:“GET”和“POST”:


GET:在使用GET命令时,只需将参数附在URL的结尾处即可。这种URL的格式如下:

http://host/path?query
  1. 每个参数都具有“名字=值”的形式,参数之间用“&”字符分隔开;
  2. 参数的值将遵循下面的规则:
    1. 保留字符“A — Z 、a — z 、0 — 9 ”,以及“.”、“-”、“~”、“_”;
    2. 用“+”字符替换所有的空格;
    3. 将其他所有字符编码为“UTF-8”,并将每个字节都编码为“%”后面紧跟一个两位的十六进制数字。
  • 老式的浏览器和代理对在“GET”请求中能够包含的字符数扯做出了限制。正因为此,POST请求经常用来处理具有大批数据的表单。


POST:在POST请求中,不会在 URL 上附着参数,而是从“URLConnection”中获得输出流,并将“名/值”对写入到该输出流中。

  • 仍旧需要对这些值进行URL编码,并用“&”字符将它们隔开;
  1. 创建一个“URLConnection”对象:
    URL url = new URL("http://host/path");􀀁
    URLConnection connection = url.openConnection();
    
  2. 调用“setDoOutput”方法建立一个用于输出的连接:
    connection.setDoOutput(true);
    
  3. 接着,调用“getOutputStream”方法获得一个流,可以通过这个流向服务器发送数据:
    如果要向服务器发送文本信息,那么可以非常方便地将流包装在“PrintWriter”对象中。
    PrintWriter out = new PrintWriter(connection.getOutputStream(), "UTF-8");􀀁
    
  4. 向服务器发送数据:
    out.print(namel + "=" + URLEncoder.encode(valuel, "UTF-8") + "&");
    out.print(name2 + "=" + URLEncoder.encode(value2, "UTF-8"));􀀁
    
  5. 关闭输出流:
    out.close();
    
  6. 最后,调用“getInputStream”方法读取服务器的响应。


人工实现重定向

  1. 在连接到服务器之前,将自动重定向关闭:
    connection.setlnstanceFollowRedirects(false);
    
  2. 在发送请求之后,获取响应码:
    int responseCode = connection.getResponseCode();􀀁
    
    检查它是否是下列值之一:
    1. HttpURLConnection.HTTP_MOVED_PERM
    2. HttpURLConnection.HTTP_MOVED_TEMP
    3. HttpURLConnection.HTTP_SEE_OTHER
  3. 如果是这些值之一,那么获取Location响应头,以获得通定向的URL。然后,断开连接,并创建到新的URL的连接:
    String location = connection.getHeaderFie1d("Location");
    if (location != nu11)
    {
       URL base = connection.getURL();
       connection.disconnect();
       connection = (HttpURLConnection) new URL(base, 1ocation).openConnection();
       . . .
    }
    

相关方法

java.net.HttpURLConnection 1.0􀀁

  • InputStream getErrorStream()􀀁
    返同一个流,通过这个流可以读取Web服务器的错误信息。

java.net.URLEncoder 1.0􀀁

  • static String encode(String s, String encoding) 1.4􀀁
    采用指定的字符编码模式(惟荐使用"UTF-8")对字符串 s进行编码, 并返回它的URL编码形式。在 URL 编码中,'A '-'Z','a'- 'z','0'- '9' '-' , _','.'和 '*'等字符保持不变,空格被编码成 '+', 所有其他字符被编码成 "%XY"形式的字节序列,其中OxXY为该字节十六进制数。

java.net.URLDecoder􀀁1.2

  • static string decode(String s, String encoding) 1.4􀀁
    采用指定编码模式对已编码宁符串s进行解码,并返回结果。

发送E-mail(JavaMail API)

利用 JavaMail API 在Java 程序中发送 E -mail :

  1. 邮件服务器相关的属性文件:
    mail.transport.protocol=smtps􀀁
    mail.smtps.auth=true􀀁
    mail.s111tps.host=smtp.gmail.com􀀁
    mail.smtps.user=cayhorstmann@gmail.com􀀁
    
  2. 读入属性文件,然后获取一个邮件会话:
    Session mai1Session = Session.getDefaultInstance(props);􀀁
    
  3. 用恰当的发送者、接受者、主题和消息文本来创建消息:
    MimeMessage message = new MimeMessage(mailSession);
    message.setFrom(new InternetAddress(from));
    message.addRecipient(RecipientType.TO, new InternetAddress(to));
    message.setSubject(subject);
    message.setText(bui1der.toString());
    
  4. 将消息发送走:
    Transport tr = mailSession.getTransport();
    tr.connect(nu11, password);
    tr.sendMessage(message, message.getA11Recipients());
    tr.close();