Java网络编程

Java 最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去,并隐藏在JVM 以及Java 的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型;事实上,网络连接(一个“套接字”)已被封装到系统对象里除此以外,在我们处理另一个连网问题——同时控制多个网络连接——的时候,Java 内建的多线程机制也是十分方便的。

1 5 . 1 机器的标识

利用由java.net 提供的static InetAddress.getByName(),我们可以让一个特定的Java 对象表达上述任何一种形式的数字。结果是类型为InetAddress 的一个对象,可用它构成一个“套接字”(Socket)。

作为运用InetAddress.getByName()一个简单的例子,请考虑假设自己有一家拨号连接因特网服务提供(ISP),那么会发生什么情况。每次拨号连接的时候,都会分配得到一个临时IP 地址。但在连接期间,那个IP 地址拥有与因特网上其他IP 地址一样的有效性。如果有人按照你的IP 地址连接你的机器,他们就有可能使用在你机器上运行的Web 或者FTP 服务器程序。当然这有个前提,对方必须准确地知道你目前分配到的IP。由于每次拨号连接获得的IP 都是随机的,怎样才能准确地掌握你的IP 呢?

java whoAmI Colossus得到的结果象下面这个样子(当然,这个地址可能每次都是不同的):
Colossus/202.98.41.151   假如我把这个地址告诉一位朋友,他就可以立即登录到我的个人Web 服务器,只需指定目标地址http://202.98.41.151 即可(当然,我此时不能断线)。有些时候,这是向其他人发送信息或者在自己的Web 站点正式出台以前进行测试的一种方便手段。

1 5 . 1 . 1 服务器和客户机

所以服务器的主要任务是侦听建立连接的请求,这是由我们创建的特定服务器对象完成的。而客户机的任务
是试着与一台服务器建立连接,这是由我们创建的特定客户机对象完成的。一旦连接建好,那么无论在服务
器端还是客户机端,连接只是魔术般地变成了一个IO 数据流对象。从这时开始,我们可以象读写一个普通的
文件那样对待连接。所以一旦建好连接,我们只需象第10 章那样使用自己熟悉的IO 命令即可。这正是Java
连网最方便的一个地方。

一个特殊的地址——localhost——来满足非网络环境中的测试要求。在Java 中产生这个地址最一般的做法是:
InetAddress addr = InetAddress.getByName(null);  创建InetAddress 的唯一途径就是那个类的static(静态)成员方法getByName()(这是最常用的)、getAllByName()或者getLocalHost()。

为得到本地主机地址,亦可向其直接传递字串"localhost":InetAddress.getByName("localhost");或者使用它的保留IP 地址(四点形式),就象下面这样:InetAddress.getByName("127.0.0.1");

1 5 . 1 . 2 端口:机器内独一无二的场所

一般每个端口都运行着一种服务,一台机器可能提供了多种服务,比如HTTP 和FTP 等等。通常,每个服务都同一台特定服务器机器上的一个独一无二的端口编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。

1 5 . 2 套接字

用于表达两台机器间一个连接的“终端”。针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知的。抽象的基本宗旨是让我们尽可能不必知道那些细节。

在Java 中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个InputStream 以
及OutputStream(若使用恰当的转换器,则分别是Reader 和Writer),以便将连接作为一个IO 流对象对待。有两个基于数据流的套接字类:ServerSocket,服务器用它“侦听”进入的连接;以及Socket,客户用它初始一次连接。

一旦客户(程序)申请建立一个套接字连接,ServerSocket 就会返回(通过accept()方法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字”连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用getInputStream()以及getOutputStream()从每个套接字产生对应的InputStream 和OutputStream 对象。这些数据流必须封装到缓冲区内。

ServerSocket 的主要任务是在那里耐心地等候其他机器同它连接,再返回一个实际的Socket。这正是“ServerSocket”这个命名不恰当的地方,因为它的目标不是真的成为一个Socket,而是在其他人同它连接的时候产生一个Socket 对象。

创建一个ServerSocket 时,只需为其赋予一个端口编号。不必把一个IP 地址分配它,因为它已经在自己代表的那台机器上了。但在创建一个Socket 时,却必须同时赋予IP 地址以及要连接的端口编号(另一方面,从ServerSocket.accept()返回的Socket 已经包含了所有这些信息)。

1 5 . 2 . 1 一个简单的服务器和客户机程序

这个例子将以最简单的方式运用套接字对服务器和客户机进行操作。服务器的全部工作就是等候建立一个连接,然后用那个连接产生的Socket 创建一个InputStream 以及一个OutputStream。在这之后,它从InputStream 读入的所有东西都会反馈给OutputStream,直到接收到行中止(END)为止,最后关闭连接。客户机连接与服务器的连接,然后创建一个OutputStream。文本行通过OutputStream 发送。客户机也会创建一个InputStream,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。

服务器与客户机(程序)都使用同样的端口号,而且客户机利用本地主机地址连接位于同一台机器中的服务

public class JabberServer {
// Choose a port outside of the range 1-1024:
public static final int PORT = 8080;
public static void main(String[] args)
throws IOException {
ServerSocket s = new ServerSocket(PORT);
System.out.println("Started: " + s);
try {
// Blocks until a connection occurs:
Socket socket = s.accept();
try {
System.out.println(
"Connection accepted: "+ socket);
BufferedReader in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed
// by PrintWriter:
PrintWriter out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())),true);
while (true) {
String str = in.readLine();
if (str.equals("END")) break;
System.out.println("Echoing: " + str);
out.println(str);
}
// Always close the two sockets...
} finally {
System.out.println("closing...");
socket.close();
}
} finally {
s.close();
}
}
} ///:~

可以看到,ServerSocket 需要的只是一个端口编号,不需要IP 地址(因为它就在这台机器上运行)。调用
accept()时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里
等候连接,但其他进程仍能正常运行(参考第14 章)。建好一个连接以后,accept()就会返回一个Socket
对象,它是那个连接的代表。

若ServerSocket 构建器成功执行,则其他所有方法调用都必须到一个try-finally 代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket 都能正确地关闭。

由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它们清除。

程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是InputStream 和OutputStream 是从Socket 对象创建的。利用两个“转换器”类InputStreamReader 和OutputStreamWriter ,InputStream 和OutputStream 对象已经分别转换成为Java 1.1 的Reader 和Writer 对象。也可以直接使用Java1.0 的InputStream 和OutputStream 类,但对输出来说,使用Writer 方式具有明显的优势。这一优势是通过PrintWriter 表现出来的,它有一个过载的构建器,能获取第二个参数——一个布尔值标志,指向是否在每一次println()结束的时候自动刷新输出(但不适用于print()语句)。每次写入了输出内容后(写进out),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。

//: JabberClient.java
// Very simple client that just sends
// lines to the server and reads lines
// that the server sends.
import java.net.*;
import java.io.*;
public class JabberClient {
public static void main(String[] args)
throws IOException {
// Passing null to getByName() produces the
// special "Local Loopback" IP address, for
542
// testing on one machine w/o a network:
InetAddress addr =
InetAddress.getByName(null);
// Alternatively, you can use
// the address or name:
// InetAddress addr =
// InetAddress.getByName("127.0.0.1");
// InetAddress addr =
// InetAddress.getByName("localhost");
System.out.println("addr = " + addr);
Socket socket =
new Socket(addr, JabberServer.PORT);
// Guard everything in a try-finally to make
// sure that the socket is closed:
try {
System.out.println("socket = " + socket);
BufferedReader in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed
// by PrintWriter:
PrintWriter out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())),true);
for(int i = 0; i < 10; i ++) {
out.println("howdy " + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
} finally {
System.out.println("closing...");
socket.close();
}
}
} ///:~

一旦客户程序发出请求,机器上下一个可用的端口就会分配给它(这种情况下是1077),这一行动也在与服务程序相同的机器(127.0.0.1)上进行。现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到哪里。所以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况:
Socket[addr=127.0.0.1,port=1077,localport=8080]。这意味着服务器刚才已接受了来自127.0.0.1 这台机器的端口1077 的连接,同时监听自己的本地端口(8080)。而在客户端:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]。这意味着客户已用自己的本地端口1077 与127.0.0.1 机器上的端口8080 建立了连接。

套接字建立了一个“专用”连接,它会一直持续到明确断开连接为止(专用连接也可能间接性地断开,前提
是某一端或者中间的某条链路出现故障而崩溃)。这意味着参与连接的双方都被锁定在通信中,而且无论是
否有数据传递,连接都会连续处于开放状态。从表面看,这似乎是一种合理的连网方式。然而,它也为网络
带来了额外的开销。本章后面会介绍进行连网的另一种方式。采用那种方式,连接的建立只是暂时的。

。由于Java 的线程处理方式非常直接,所以让服务器控制多名客户并不是件难事。最基本的方法是在服务器(程序)里创建单个ServerSocket,并调用accept()来等候一个新连接。一旦accept()返回,我们就取得结果获得的Socket,并用它新建一个线程,令其只为那个特定的客户服务。然后再调用accept() ,等候下一次新的连接请求。

//: MultiJabberServer.java
// A server that uses multithreading to handle
// any number of clients.
import java.io.*;
import java.net.*;
class ServeOneJabber extends Thread {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
public ServeOneJabber(Socket s)
throws IOException {
socket = s;
in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Enable auto-flush:
544
out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())), true);
// If any of the above calls throw an
// exception, the caller is responsible for
// closing the socket. Otherwise the thread
// will close it.
start(); // Calls run()
}
public void run() {
try {
while (true) {
String str = in.readLine();
if (str.equals("END")) break;
System.out.println("Echoing: " + str);
out.println(str);
}
System.out.println("closing...");
} catch (IOException e) {
} finally {
try {
socket.close();
} catch(IOException e) {}
}
}
}

public class MultiJabberServer {
static final int PORT = 8080;
public static void main(String[] args)
throws IOException {
ServerSocket s = new ServerSocket(PORT);
System.out.println("Server Started");
try {
while(true) {
// Blocks until a connection occurs:
Socket socket = s.accept();
try {
new ServeOneJabber(socket);
} catch(IOException e) {
// If it fails, close the socket,
// otherwise the thread will close it:
socket.close();
}
}
} finally {
s.close();
}
}

同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在ServeOneJabber 外部创建
的,所以清除工作可以“共享”。若ServeOneJabber 构建器失败,那么只需向调用者“掷”出一个违例即
可,然后由调用者负责线程的清除。但假如构建器成功,那么必须由ServeOneJabber 对象负责线程的清除,
这是在它的run()里进行的。

请注意MultiJabberServer 有多么简单。和以前一样,我们创建一个ServerSocket,并调用accept()允许一
个新连接的建立。但这一次,accept() 的返回值(一个套接字)将传递给用于ServeOneJabber 的构建器,由
它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。

为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的
服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许
创建的线程的最大数量是由final int maxthreads 决定的。大家会注意到这个值非常关键,因为假如把它设
得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。

 

你可能感兴趣的:(java,职场,网络编程,休闲)