第3章 网络
__3_1 连接到服务器
使用telnet来连接远程计算机
telnet time-A.timefreq.bldrdoc.gov 13
打开一个套接字,套接字是网络软件中的一个抽象概念,负责使能该程序内部和外部之间的通信。我们将远程地址和端口传递给套接字的构造器,如果连接失败,它将抛出一个UnknowHostException异常;如果存在其他问题,它将抛出一个IOException异常。
一旦套接字被打开,java.net.Socket类的getInputStream方法就会返回一个InputStream对象。
(本书所介绍的内容近使用于TCP(传输控制协议)网络协议。Java平台支持UDP(用户数据报协议)协议,UDP付出的开销要比TCP少得多。UDP的数据包无需按照顺序进行传递,它甚至可能在传输过程中全部丢失。UDP比较适合于音频流和视频流的传输,或者用于连续测量的应用领域。)
Socket s = new Socket("time-A.timefreq.bldrdoc.gov",13);
InputStream inStream = s.getInputStream();
1)套接字超时
从套接字读取信息时,在可以访问数据之前,读操作将会被阻塞。如果此时主机不可达,那么应用将要等待很长时间,并且因为受底层操作系统的限制而最终会导致超时。
调用setSoTimeout方法设置这个超时值,单位为毫秒。如果已经为套接字设置了超时值,并且之后的读操作和写操作在没有完成之前就超过了时间限制,这些操作会抛出SocketTimeoutException异常。
Socket s = new Socket(...);
s.setSoTimeout(10000);
try
{
InputStream in = s.getInputStream(); // read from in
...
}
catch(InterruptedIOException exception)
{
react to timeout
}
另外,构造器:
Socket(String host, int port)
会一直无限期的阻塞下去,直到建立了到达主机的初始连接为止。
可以通过先构造一个无连接的套接字,然后再使用一个超时来进行连接的方法。
Socket s = new Socket();
s.connect(new InetSocketAddress(host, port), timeout);
//Socket类的主要方法
Socket()
//创建一个未被链接的套接字
void connect(SocketAddress address)
//将该套接字连接到给定的地址
void connect(SocketAddress address, int timeoutInMilliseconds)
//将该套接字连接到给定的地址,如果在给定时间没有响应,则返回。
void setSoTimeout(int timeoutInMilliseconds)
//设置该套接字上读请求的阻塞时间。如果超出给定时间,则抛出一个InterruptIOException异常
2) 因特网地址
因特网地址是指用一串数字表示的主机地址。如果需要在主机名和因特网地址之间进行转换时,可以使用InetAddress类。
只要主机操作系统支持IPv6格式的因特网地址,java.net包也将支持它。
静态的getByName方法可以返回代表某个主机的InetAddress对象。例如,
InetAddress address = InetAddress.getByName("time-A.timefreq.bldrdoc.gov");
将返回一个InetAddress对象,该对象封装了一个4字节的序列:132.163.4.104。然后,可以使用getAddress方法来访问这些字节。
byte[] addressBytes = address.getAddress();
一些访问量较大的主机名通常会对应多个因特网地址,当访问主机时,其因特网地址将这三者中随机产生。可以通过调用getAllByName方法来获得所有主机。
InetAddress[] addresses = InetAddress.getAllByName(host);
如果只是要求得到localhost的地址,那总会得到127.0.0.1。可以使用getLocalHost方法来得到本地主机的地址。
InetAddress adress = InetAddress.getLocalHost();
仿照书中做练习如下,输出本机的因特网地址以及命令行参数中主机名对应的的因特网地址。
package learn.test.net;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressTest
{
public static void main(String[] args)
{
try
{
System.out.println("Local Host IP = " + InetAddress.getLocalHost().getHostAddress());
}
catch (UnknownHostException e1)
{
e1.printStackTrace();
}
if(args.length > 0)
{
for(String str : args)
{
System.out.println("Host name = \"" + str + "\"");
InetAddress[] addrs = new InetAddress[0];
try
{
addrs = InetAddress.getAllByName(str);
}
catch (UnknownHostException e)
{
e.printStackTrace();
}
for(InetAddress addr : addrs)
System.out.println(addr.getHostAddress());
}
}
}
}
__3_2 实现服务器
ServerSocket类用于建立一个套接字,
ServerSocket s = new ServerSocket(8189);
用于建立一个负责监听端口8189的服务器。以下命令:
Socket incoming = s.accept();
用于告诉程序不停地等待,直到有客户端连接这个端口。一旦有人通过网络向该端口发送了正确的连接请求,该方法就会返回一个表示连接已经建立的Socket对象。
服务器发送给服务器输出流的所有信息都会成为客户端程序的输入,同时,来自客户端程序的所有输出都会被包含在服务器输入流中。
若传送文本信息,将流转换成扫描器和写入器。
Scanner in = new Scanner(inStream);
PrintWriter out = new PrintWriter(outStream, true/* autoFulash*/);
使用一下代码给客户端发送一条问候信息:
out.println("Hello! Enter BYE to exit.");
当使用telnet通过端口8189连接到这个服务器程序时,将会在终端屏幕上看到上述问候信息。
在代码的最后应关闭连接进来的套接字。
每一个服务器程序,比如一个HTTP Web服务器,都不间断地执行下面这个循环操作:
- 通过输入数据流从客户端接收一个命令
- 解码这个命令
- 收集客户端所请求的信息
- 通过输出数据流发送信息给客户端
仿照书中做练习如下:
package learn.test.net;
import java.io.IOException;
import java.io.PrintStream;
import java.net.*;
import java.util.Scanner;
public class EchoServerTest
{
public static void main(String[] args)
{
try
{
ServerSocket server = new ServerSocket(1888);
Socket s = server.accept();
try
{
PrintStream out = new PrintStream(s.getOutputStream());
Scanner in = new Scanner(s.getInputStream());
while(in.hasNextLine())
{
String str = in.nextLine().trim();
if(!str.equalsIgnoreCase("quit"))
out.println("ECHO: " + str);
else
break;
}
}
finally
{
s.close();
}
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
1)为多个客户端服务
每当程序建立一个新的套接字连接,也就是说当成功调用accept的时候,将创建一个新的线程来处理服务器和该客户端之间的连接,而主程序将返回等待下一个连接。
为每一个连接生成一个独立的线程,这种方法并不能满足高性能服务器的要求。为使服务器实现更高的吞吐量,你可以使用java.nio包中一些特性。
练习如下:
package learn.test.net;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class ThreadEchoServerTest
{
public static void main(String[] args)
{
try
{
ServerSocket server = new ServerSocket(1999);
try
{
while(true)
{
Socket incoming = server.accept();
new Thread(new EchoThread(incoming)).start();
}
}
finally
{
server.close();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
class EchoThread implements Runnable
{
public EchoThread(Socket s)
{
this.s = s;
try
{
in = new Scanner(s.getInputStream());
out = new PrintStream(s.getOutputStream());
}
catch (IOException e)
{
e.printStackTrace();
}
}
public void run()
{
while(in.hasNext())
{
String str = in.nextLine().trim();
if(!str.equalsIgnoreCase("quit"))
out.println("ECHO-[" + Thread.currentThread().getName() + "] : " + str);
else
break;
}
try
{
s.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
Socket s = null;
Scanner in = null;
PrintStream out = null;
}
2)半关闭
半关闭(half-close)提供了这样一种能力:套接字连接的一端可以终止其输出,同时仍可以接收来自另一端的数据。
该协议只使用于一站式(one-shot)的服务,例如HTTP服务,在这种服务中,客户端连接服务器,发送个一个请求,捕获响应信息,然后断开连接。
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();
//socket.isOutputShutdown()方法检查是否关闭输出
//now socket is half closed
//read response data
while(in.hasNextLine() != null)
{
String line = in.nextLine();
...
}
socket.close();
__3_3 可中断套接字
当连接到一个套接字时,当前线程将会被阻塞直到建立连接或产生超时为止。当通过套接字读写数据时,当前线程也会被阻塞直到操作成功或产生超时为止。
为了中断套接字操作,可以使用java.nio包提供的一个特性——SocketChannel类。可以使用如下方法打开SocketChannel:
SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));
通道(channel)并没有与之相关联的流。实际上,它所拥有的read和write方法都是通过调用Buffer对象来实现的。ReadableByteChannel接口和WritableByteChannel接口都声明了这两个方法。
如果不想处理缓存,可以使用Scanner类来读取信息,因为Scanner有一个带ReadableByteChannel参数的构造器:
Scanner in = new Scanner(channel);
通过调用静态方法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)
//打开一个套接字通道,并将其连接到远程地址