<!---->1.1 <!---->连接到服务器
telnet 这个工具,我想很多人都用过,telnet xxx.com 80 即返回该网站的html 格式的数据,在Java 中Socket 类就类似于这个工具的功能。下面举个例子:
package net.socket; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.util.Scanner; /** * 连接到某个端口服务器,并找印它所返回的信息 * * @author Janwer */ public class SocketTest { public static void main(String[] args) { try { //用于打开一个套接字 Socket s = new Socket("www.baidu.com",80); try { InputStream inStream = s.getInputStream(); //一个可以使用正则表达式来分析基本类型和字符串的简单文本扫描器 Scanner in = new Scanner(inStream); while(in.hasNextLine()) { String line = in.nextLine(); System.out.println(line); } }finally { s.close(); } }catch(IOException e) { e.printStackTrace(); } } }
<!---->1.2 <!---->建立URL 连接
API Java.net.URL
打开一个用于读取资源数据的输入流
InputStream openStream()
返回一个URLConnection 对象,该对象负责管理与资源之间的连接
URLConnection openConection()
当操作一个URLConnection 对象时,须非常小心地安排操作步骤:
1>. 调用URL 类中的openConnection 方法获得URLConnection 对象:
URLConnection connection = url.openConnection();
2>. 操作设置参数和一般请求属性:( 使用URLConnection 类比基本的URL 类能得到更多的控制功能)
API Java.net.URLConnection
如果doInput 为true ,那么用户可以接收来自该URLConnection 的输入
void setDoInput(boolean doInput)
Boolean getDoInput()
如果doOutput 为true ,那么用户可以将输出发送到该URLConnection
Void setDoOutput(boolean doOutput)
Boolean getDoOutput()
属性ifModifiedSince 用于配置URLConnection 对象,使它只获取那些自从某个给定时间以来被修改过的数据,调用方法时传入一个long 型的秒数
Void setIfModifiedSince (long time)
Long getIfModifiedSince ()
如果useCaches 为true ,那么数据可以从本地缓存中得到。注意,URLConnection 本身并不维护这个缓存,须由浏览器之类的外部程序提供。
void setUseCaches(boolean useCaches)
boolean getUseCaches()
若allowUserInteraction 为true ,那么可以查询用户密码,也须由外部提供
void setAllowUserInteraction(boolean allowUserInteraction)
boolean getAllowsUserInteraction()
设置一个连接超时时限( 单位:毫秒) 。如果达到时限将抛出SocketTimeoutException
void setConnectTimeout(int timeout)
int getConnectTimeout()
读取超时时限(单位:毫秒),同样如达时限抛出SocketTimeoutException
void setReadTimeout(int timeout)
int getReadTimeout()
设置请求头的一个字段
void setRequestProperty(String key, String value)
返回请求头属性的一个映射表,相同的键对应的所有值被放置在同一个列表中
Map<String,List<String>> getRequestProperties()
连接远程资源并获取响应头信息
void connect()
返回响应的一个映射表
Map<String,List<String>> getHeaderFields()
得到响应头第n 个字段的键/ 值,如果n 等于0 或大于响应头字段总数,该方法返回null 值
String getHeaderFieldKey(int n) / String getHeaderField(int n)
如果知道内容长度,则返回该长度值,否则返回-1
int getContentLength()
获取内容的类型,比如text/plain 或image/gif
String getContentType()
获取内容的编码,比如gzip 。这个值不太常用,因为默认的identity 编码并不是用Content-Encoding 头来设定的
String getContentEncoding()
获取创建日期、过期日以及最后一次被修改的日期。这些日期是国际标准时间的秒数
long getDate()
long getExpiration()
long getLastModifed()
返回从资源读取的信息或向资源写入信息流
InputStream getInputStream()
OutputStream getOutputStream()
选择适当的内容处理器,以便读取资源数据并将它转换成对象。该方法不能用于读取诸如text/plain 或image/gif 之类的标准内容类型,除非你安装了自己的内容处理器
Object getContent()
3>. 使用 connect 方法建立到远程对象的实际连接
connection.connect();
除了与服务器建立套接字连接外,该方法还可以用于向服务器查询头信息
4>. 远程对象变为可用。远程对象的头字段和内容变为可访问
getHeaderFieldKey 和getHeaderField 两个方法列举了消息头的所有字段。
如下示例清单:
import java.io.*; import java.net.*; import java.util.*; public class URLConnectionTest { public static void main(String[] args) { try { String urlName; if (args.length > 0) urlName = args[0]; else urlName = "http://java.sun.com"; URL url = new URL(urlName); URLConnection connection = url.openConnection(); // set username, password if specified on command line if (args.length > 2) { String username = args[1]; String password = args[2]; String input = username + ":" + password; String encoding = base64Encode(input); connection.setRequestProperty("Authorization", "Basic " + encoding); } connection.connect(); // print header fields Map<String, List<String>> headers = connection.getHeaderFields(); for (Map.Entry<String, List<String>> entry : headers.entrySet()) { String key = entry.getKey(); for (String value : entry.getValue()) System.out.println(key + ": " + value); } // print convenience functions System.out.println("----------"); System.out.println("getContentType: " + connection.getContentType()); System.out.println("getContentLength: " + connection.getContentLength()); System.out.println("getContentEncoding: " + connection.getContentEncoding()); System.out.println("getDate: " + connection.getDate()); System.out.println("getExpiration: " + connection.getExpiration()); System.out.println("getLastModifed: " + connection.getLastModified()); System.out.println("----------"); Scanner in = new Scanner(connection.getInputStream()); // print first ten lines of contents for (int n = 1; in.hasNextLine() && n <= 100; n++) System.out.println(in.nextLine()); if (in.hasNextLine()) System.out.println(". . ."); } catch (IOException e) { e.printStackTrace(); } } /** * Computes the Base64 encoding of a string * * @param s * a string * @return the Base 64 encoding of s */ public static String base64Encode(String s) { ByteArrayOutputStream bOut = new ByteArrayOutputStream(); Base64OutputStream out = new Base64OutputStream(bOut); try { out.write(s.getBytes()); out.flush(); } catch (IOException e) { } return bOut.toString(); } } /** * This stream filter converts a stream of bytes to their Base64 encoding. * * Base64 encoding encodes 3 bytes into 4 characters. * |11111122|22223333|33444444| Each set of 6 bits is encoded according to the * toBase64 map. If the number of input bytes is not a multiple of 3, then the * last group of 4 characters is padded with one or two = signs. Each output * line is at most 76 characters. */ class Base64OutputStream extends FilterOutputStream { /** * Constructs the stream filter * * @param out * the stream to filter */ public Base64OutputStream(OutputStream out) { super(out); } public void write(int c) throws IOException { inbuf[i] = c; i++; if (i == 3) { super.write(toBase64[(inbuf[0] & 0xFC) >> 2]); super.write(toBase64[((inbuf[0] & 0x03) << 4) | ((inbuf[1] & 0xF0) >> 4)]); super.write(toBase64[((inbuf[1] & 0x0F) << 2) | ((inbuf[2] & 0xC0) >> 6)]); super.write(toBase64[inbuf[2] & 0x3F]); col += 4; i = 0; if (col >= 76) { super.write('\n'); col = 0; } } } public void flush() throws IOException { if (i == 1) { super.write(toBase64[(inbuf[0] & 0xFC) >> 2]); super.write(toBase64[(inbuf[0] & 0x03) << 4]); super.write('='); super.write('='); } else if (i == 2) { super.write(toBase64[(inbuf[0] & 0xFC) >> 2]); super.write(toBase64[((inbuf[0] & 0x03) << 4) | ((inbuf[1] & 0xF0) >> 4)]); super.write(toBase64[(inbuf[1] & 0x0F) << 2]); super.write('='); } } private static char[] toBase64 = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; private int col = 0; private int i = 0; private int[] inbuf = new int[3]; }
在默认情况下建立的连接只有从服务器读取信息的输入流,并没有任何执行写操作的输出流。如果想获得输出流(例如,向一个Web 服务器提交数据), 那么需要调用:connection.setDoOutput(true) 还可以设置一些请求头与请求命令,这些属性将一起被发送到服务器。
使用setRequestProperty 来设置对特定协议起作用的任何“名- 值对”。关于HTTP 请求头的格式,请参见RFC 2616 ,其中的某些参数没有被记录在文档中,它们通常在程序员之间口头传递。E.g. ,你想访问一个有密码保护的web 页,那么必须按如下步骤操作:
1>. 将用户名、冒号和密码以字符串形式连接在一起。
String input = username + “:” + password;
2>. 计算上一步骤所得字符的base64 编码。(base64 编码用于将字节流编码成可打印的ASCII 字符流)
String encoding = base64Encode(input);
3>. 调用setRequestProperty 方法,设置name 参数的值为"Authorization" 、value 参数的值为"Basic" + encoding ,如下:
connection.setRequestProperty("Authorization", "Basic" + encoding);
当然上面提及的只是一种访问有密码保护的web 页,如果想要通过FTP 访问一个有密码保护的文件,要采用一种完全不同的方法,可以直接构建一个如下格式的URL:
ftp://username:[email protected]/pub/file.txt
1.3 提交表单数据
使用POST 命令时,并不需在URL 中添加任何参数,而从URLConnection 中获取输出流,将name/value 对写入该流中,需对这些值进行URL 编码,并用& 字符将它们隔开。下面将介绍这个过程:
首先创建一个URLConnection 对象
URL url = new URL(http://host/script );
URLConnection connection = url.openConnection();
然后调用setDoOutput 方法建立一个用于输出的连接。
connection.setDoOutput(true);
接着,调用getOutputStream 方法获得一个流,可以通过这个流向服务器发送数据。如果要向服务器发送文本信息,那么可以非常方便地将流包装在PrintWriter 对象中。
PrintWriter out = new PrintWriter(connection.getOutputStream());
现在可以向服务器发送数据了:
out.print(name1+”=” + URLEncoder.encode(value1,”UTF-8”)+”&”);
out.print(name2 + “=” URLEncoder.encode(value2,”UTF-8”));
之后,关闭输出流。
out.close();
最后调用getInputStream 方法读取服务器的响应。如果在读取过程中碰到脚本运行错误,那么服务器会返回一个错误页面。为了捕捉这个错误页,可以将URLConnection 对象转型为HttpURlConnection 类开调用它的getErrorStream 方法:
InputStream err = ((HttpURLConnection)connection.getErrorStream();
如下示例:
package net; import java.io.*; import java.net.*; import java.util.*; /** * @author Janwer * */ public class DoPostTest { public static void main(String[] args)throws IOException { final String SERVER_URL = "http://www.census.gov/cgi-bin/ipc/idbagg"; Map<String, String> post = new HashMap<String, String>(); post.put("tbl", "001"); post.put("vgp", "1"); post.put("aggtogether", "no"); post.put("levout", "agg"); post.put("reg", "074"); post.put("cty","CH"); post.put("yr", "2008"); post.put("fdel", "b"); post.put("deltxt", ""); post.put("xlate", "all"); post.put("pres", "commas"); post.put("stubf", "all"); try { String re = doPost(SERVER_URL, post); System.out.print(re); } catch (IOException e) { e.printStackTrace(); } } public static String doPost(String urlString, Map<String, String> nameValuePairs) throws IOException { //打开链接 URL url = new URL(urlString); URLConnection connection = url.openConnection(); //建立一个输出流 connection.setDoOutput(true); PrintWriter out = new PrintWriter(connection.getOutputStream()); boolean first = true; for (Map.Entry<String, String> pair : nameValuePairs.entrySet()) { if (first) first = false; else out.print('&'); String name = pair.getKey(); String value = pair.getValue(); out.print(name); out.print('='); out.print(URLEncoder.encode(value, "UTF-8")); } out.close(); Scanner in; StringBuilder response = new StringBuilder(); try { in = new Scanner(connection.getInputStream()); } catch (IOException e) { //捕捉服务器端的脚本运行异常 if (!(connection instanceof HttpURLConnection)) throw e; InputStream err = ((HttpURLConnection) connection).getErrorStream(); if (err == null) throw e; in = new Scanner(err); } //开始读取服务器端的反馈信息 while (in.hasNextLine()) { response.append(in.nextLine()); response.append("\n"); } in.close(); return response.toString(); } }
API Java.net.HttpURLConnection
InputStream getErrorStream()
返回一个流,通过这个流可以读取web 服务器的错误信息
API Java.net.URLEncoder
static String encode(String s, String encoding)
采用指定的字符编码模式,对字符串s 进行编码,并返回它的URL 编码形式。在URL 编码中,’A’-‘Z’ ,’a’-‘z’ ,’0’-‘9’ ,’-‘ ,’_’ ,’.’ 和’*’ 等字符保持不变,空格被编码成’+’ ,所有其他字符被编码成”%XY” 形式的字节序列,其中0xXY 为该字节十六进制数。
API Java.net.URLDecoder
static String decode(String s, String encoding)
采用指定编码模式对已编码字符串s 进行解码,并返回结果。
1.4 高级套接字编程
1.4.1 套接字超时
在实际应用中,你可能并不想从套接字读取信息,因为在数据可以被访问之前,读操作将会阻塞。如果此时主机不可达,那么你的应用将要等待很长的时间,并且因为受底层操作系统的限制而最终会导致超时。所以针对不同的应用,应该确定合理的超时时限。调用setSoTimeout 方法可以设置这个超时时限(单位:毫秒)
方式一:
Socket s = new Socket(....);
s.setSoTimeout(1000); //time out after 10 seconds
方式二:
Socket s = new Socket();
s.connect(new InetSocketAdress(host,port),timeout);
如果你已经为套接字设置了超时值,并且之后的读操作和写操作在没有被完成之前就超过了时间限制,那么这些操作就会抛出SocketTimeoutException 异常。你可以捕获这个异常,并对超时做出反应。
try {
Scanner in = new Scanner(s.getInputStream());
String line = in.next();
...
}catch(InterruptedIOException exception) {
reach to timeout
}
1.4.2 可中断套接字
在交互式的应用中,也许会考虑为用户提供一个功能,用以取消那些看似不会成功的连接。但是,当线程因套接字长时间无法响应而发生阻塞时,无法通过调用interrupt 来解除阻塞。
为了中断套接字操作,可以使用java.nio 包提供的一个特性―――socketChannel 类,可以使用如下方法打开:
SocketChannel channel = SocketChannel.open(new InetSocketAddress(host,port));
如果不想处理缓存,可以使用Scanner 类来读取信息,因为Scanner 有一个带ReadableByteChannel 参数的构造器:Scanner in = new Scanner(channel);
通过调用静态方法Channels.newOutputStream ,可以从通道中获取输出流。
OutputStream outStream = Channels.newOutputStream(channel);
上述操作都是必须的,假设线程正在执行打开、读取或写入操作,此时如果线程中断,那么这些操作将不会陷入阻塞,而是以抛出异常的方式结束。
如下示例:
import java.awt.*; import java.awt.event.*; import java.util.*; import java.net.*; import java.io.*; import java.nio.channels.*; import javax.swing.*; public class InterruptibleSocketTest { public static void main(String[] args) { JFrame frame = new InterruptibleSocketFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } } class InterruptibleSocketFrame extends JFrame { public InterruptibleSocketFrame() { setSize(WIDTH, HEIGHT); setTitle("InterruptibleSocketTest"); JPanel northPanel = new JPanel(); add(northPanel, BorderLayout.NORTH); messages = new JTextArea(); add(new JScrollPane(messages)); busyBox = new JCheckBox("Busy"); northPanel.add(busyBox); startButton = new JButton("Start"); northPanel.add(startButton); startButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { startButton.setEnabled(false); cancelButton.setEnabled(true); connectThread = new Thread(new Runnable() { public void run() { connect(); } }); connectThread.start(); } }); cancelButton = new JButton("Cancel"); cancelButton.setEnabled(false); northPanel.add(cancelButton); cancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { connectThread.interrupt(); startButton.setEnabled(true); cancelButton.setEnabled(false); } }); server = new TestServer(); new Thread(server).start(); } /** * Connects to the test server. */ public void connect() { try { //使用java.nio包提供的一个特性 SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 8189)); try { //使用Scanner可以不处理缓存问题 in = new Scanner(channel); while (true) { if (in.hasNextLine()) { String line = in.nextLine(); messages.append(line); messages.append("\n"); } else Thread.sleep(100); } } finally { channel.close(); messages.append("Socket closed\n"); } } catch (IOException e) { messages.append("\nInterruptibleSocketTest.connect: " + e); } catch (InterruptedException e) { messages.append("\nInterruptibleSocketTest.connect: " + e); } } /** * A multithreaded server that listens to port 8189 and sends random numbers * to the client. */ class TestServer implements Runnable { public void run() { try { int i = 1; ServerSocket s = new ServerSocket(8189); while (true) { Socket incoming = s.accept(); Runnable r = new RandomNumberHandler(incoming); Thread t = new Thread(r); t.start(); } } catch (IOException e) { messages.append("\nTestServer.run: " + e); } } } /** * This class handles the client input for one server socket connection. */ class RandomNumberHandler implements Runnable { /** * Constructs a handler. * * @param i * the incoming socket */ public RandomNumberHandler(Socket i) { incoming = i; } public void run() { try { //获得输出流 OutputStream outStream = incoming.getOutputStream(); PrintWriter out = new PrintWriter(outStream, true /* autoFlush */); Random generator = new Random(); while (true) { if (!busyBox.isSelected()) out.println(generator.nextInt()); Thread.sleep(100); } } catch (IOException e) { messages.append("\nRandomNumberHandler.run: " + e); } catch (InterruptedException e) { messages.append("\nRandomNumberHandler.run: " + e); } } private Socket incoming; } private Scanner in; private PrintWriter out; private JButton startButton; private JButton cancelButton; private JCheckBox busyBox; private JTextArea messages; private TestServer server; private Thread connectThread; public static final int WIDTH = 300; public static final int HEIGHT = 300; }
1.4.3 半关闭
当客户端程序发送一个请求给服务器时,服务器必须能够确定该请求何时结束。因此,许多网络协议都是面向行的。其他一些协议则包含一个消息头用以指明请求数据的大小。否则要想表示请求数据的结束将比向文件写入数据更加困难。写一个文件时,只需在数据写入后关闭文件即可。但是如果关闭一个套接字,那么将立刻断开与服务器的连接。
使用半关闭的方法就可以解决上述问题。可以通过关闭一个套接字的输出流来表示发送给服务器的请求数据已经结束,但是必须保留输入流打开用以读取服务器的反馈信息。
如下代码淙了如何在客户端使用半关闭方法:
Socket socket = new Socket(host,port);
Scanner in = new Scanner(socket.getInputStream());
PrintWriter writer = new PrintWriter(socket.getOutputStream());
//send request data
writer.print(…);
writer.flush();
socket.shutdownOutput();
//noew cocket is half closed read response data
while((in.hasNextLine()) != null) {
String line = in.nextLine();
…}
Socket.close();
服务器端将读取输入信息,直到到达输入流的结尾。
当然,该协议只适用于一站式的服务,例如HTTP 服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接。
1.4.4 因特网地址
这里介绍的仅适用于TCP 网络协议。TCP 可以在两台计算机之间建立可靠连接。Java 平台也支持UDP 协议,该协议可以用于发送数据包,所需开销要比TCP 少得多。但数据包是随机传递的,传输过程中可能丢失。UDP 要求数据包的接受者对它们进行排序,并请求发送者重新发送。UDP 比较适合于那些可以忍受数据包丢失的应用,例如音频流和视频流等。
如下示例:
import java.net.*; public class InetAddressTest { public static void main(String[] args) { try { if (args.length > 0) { String host = args[0]; InetAddress[] addresses = InetAddress.getAllByName(host); for (InetAddress a : addresses) System.out.println(a); } else { InetAddress localHostAddress = InetAddress.getLocalHost(); System.out.println(localHostAddress); } } catch (Exception e) { e.printStackTrace(); } } }
API Java.net.InetAddress
为给定的主机名,创建一个InetAddress 对象或者一个包含了该主机名所对应的所有因特网地址的数组。
static InetAddress getByName(String host)
static InetAddress[] getAllByName(String host)
为本地主机创建一个InetAddress 对象
Static InetAddress getLocalHost()
返回一个包含数字型地址的字节数据
Byte[] getAddress()
返回一个由十进制数组成的字符串,各数字间用圆点符号隔开,例如,”132.134.4.120”.
String getHostAddress()
返回主机名
String getHostName()
API Java.net.Socket
创建一个还未被连接的套接字
socket()
将该套接字连接到给定地址
void connect(SocketAddress address)
将套接字连接到给定的地址,如果给定的时间内没有响应,则返回
void connect(SocketAddress address,int timeoutInMilliseconds)
如果该套按字已经连接,则返回true
boolean isConnected()
如果套按字已经被关闭,则返回true
boolean isClosed()
在该套接字上设置读请求的阻塞时间。如果超出给定时间,则抛出一个InterruptedIOException 异常
void setSoTimeout(int timeoutInMilliseconds)
将输出 / 输入流设为“流结束”
void shutdownOutput()
void shutdownInput()
如果输出/ 输入已被关闭,则返回true
Boolean isOutputShutdown()
Boolean isInputShutdown()
API Java.net.InetSocketAddress
通过主机和端口参数创建一个地址对象,并在创建过程中解析主机名。如果主机名不能被解析,那么该地址对象的unresolved 属性将被设为true
InetSocketAddress(String hostname, int port)
如果不能解析该地址对象,则返回true
boolean isUnresolved()
API Java.nio.channels.SocketChannel
打开一个套按字通道,并将其连接到远程地址
static SocketChannel open(SocketAddress address)
API Java.nio.channels.Channels
创建一输入/ 输出流,从指定的通道读取数据
static InputStream new InputStream(ReadableByteChannel channel)
static OutputStream newOutputStream(WritableByteChannel channel)