服务器进程可以提供各种各样的服务,例如本文第一节提到的EchoServer提供的服务为:根据EchoClient发出的字符串XXX,返回字符串“echo: XXX”。除了像EchoServer这样的由用户自定义的服务外,网络上还有许多众所周知的通用服务,最典型的要算Http服务。网络应用层的协议规定了客户程序与这些通用服务器程序的通信细节,例如Http协议规定了Http客户程序发出的请求的格式,还规定了Http服务器程序发回的响应的格式。
在现实生活中,有些重要的服务机构的电话是固定的,这有助于人们方便地记住电话和获得服务,比如众所周知的电话110、120和119分别是报警、急救和火警电话。同样,在网络上有些通用的服务有着固定的端口,表3对常见的服务以及相应的协议和端口做了介绍。
表3 应用层的一些通用服务使用的端口
服务 |
端口 |
协议 |
文件传输服务 |
21 |
FTP |
远程登入服务 |
23 |
TELNET |
传输邮件服务 |
25 |
SMTP |
用于万维网(WWW)的超文本传输服务 |
80 |
HTTP |
访问远程服务器上的邮件服务 |
110 |
POP3 |
互联网消息存取服务 |
143 |
IMAP4 |
安全的超文本传输服务 |
443 |
HTTPS |
安全的远程登入服务 |
992 |
TELNETS |
安全的互联网消息存取服务 |
993 |
IMAPS |
五、用Java编写客户/服务器程序
本文介绍的Java网络程序都建立在TCP/IP协议基础上,致力于实现应用层。传输层向应用层提供了套接字Socket接口,Socket封装了下层的数据传输细节,应用层的程序通过Socket来建立与远程主机的连接以及进行数据传输。
站在应用层的角度,两个进程之间的一次通信过程从建立连接开始,接着交换数据,到断开连接结束。套接字可看做是通信线路两端的收发器,进程通过套接字来收发数据,如图17所示。

图17 套接字可看过是通信连接两端的收发器
在Java中,有三种套接字类:java.net.Socket、java.net.ServerSocket和DatagramSocket。其中Socket和ServerSocket类建立在TCP协议基础上,DatagramSocket类建立在UDP协议基础上。Java网络程序都采用客户/服务通信模式。
本节以EchoServer和EchoClient为例,介绍如何用ServerSocket和Socket来编写服务器程序和客户程序。
1.创建EchoServer
服务器程序通过一直监听端口,来接收客户程序的连接请求。在服务器程序中,需要先创建一个ServerSocket对象,在构造方法中指定监听的端口:
ServerSocket server=new ServerSocket(8000); //监听8000端口
ServerSocket的构造方法负责在操作系统中把当前进程注册为服务器进程。服务器程序接下来调用ServerSocket对象accept()方法,该方法一直监听端口,等待客户的连接请求,如果接收到一个连接请求,accept()方法就会返回一个Socket对象,这个Socket对象与客户端的Socket对象形成了一条通信线路:
Socket socket=server.accept(); //等待客户的连接请求
Socket类提供了getInputStream()方法和getOutputStream()方法,分别返回输入流InputStream对象和输出流OutputStream对象。程序只需向输出流写数据,就能向对方发送数据;只需从输入流读数据,就能接收来自对方的数据。图18演示了服务器与客户利用ServerSocket和Socket来通信的过程。

图18 服务器与客户利用ServerSocket和Socket来通信
与普通I/O流一样,Socket的输入流和输出流也可以用过滤流来装饰。在以下代码中,先获得输出流,然后用PrintWriter装饰它,PrintWriter的println()方法能够写一行数据;以下代码接着获得输入流,然后用BufferedReader装饰它,BufferedReader的readLine()方法能够读入一行数据:
OutputStream socketOut = socket.getOutputStream();
//参数true表示每写一行,PrintWriter缓存就自动溢出,把数据写到目的地
PrintWriter pw=PrintWriter(socketOut,true);
InputStream socketIn = socket.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(socketIn));
以下是EchoServer的核心代码。
import java.io.*;
import java.net.*;
public class EchoServer {
private int port=8000;
private ServerSocket serverSocket;
public EchoServer() throws IOException {
serverSocket = new ServerSocket(port);
System.out.println("服务器启动");
}
public String echo(String msg) {
return "echo:" + msg;
}
private PrintWriter getWriter(Socket socket)throws IOException{
OutputStream socketOut = socket.getOutputStream();
return new PrintWriter(socketOut,true);
}
private BufferedReader getReader(Socket socket)throws IOException{
InputStream socketIn = socket.getInputStream();
return new BufferedReader(new InputStreamReader(socketIn));
}
public void service() {
while (true) {
Socket socket=null;
try {
socket = serverSocket.accept(); //等待客户连接
System.out.println("New connection accepted "
+socket.getInetAddress() + ":" +socket.getPort());
BufferedReader br =getReader(socket);
PrintWriter pw = getWriter(socket);
String msg = null;
while ((msg = br.readLine()) != null) {
System.out.println(msg);
pw.println(echo(msg));
if (msg.equals("bye")) //如果客户发送的消息为“bye”,就结束通信
break;
}
}catch (IOException e) {
e.printStackTrace();
}finally {
try{
if(socket!=null)socket.close(); //断开连接
}catch (IOException e) {e.printStackTrace();}
}
}
}
public static void main(String args[])throws IOException {
new EchoServer().service();
}
}
EchoServer类最主要的方法为service()方法,它不断等待客户的连接请求,当serverSocket.accept()方法返回一个Socket对象,就意味着与一个客户建立了连接。接下来从Socket对象中得到输出流和输入流,并且分别用PrintWriter和BufferedReader来装饰它们。然后不断调用BufferedReader的readLine()方法读取客户发来的字符串XXX,再调用PrintWriter的println()方法向客户返回字符串echo:XXX。当客户发来的字符串为“bye”,就会结束与客户的通信,调用socket.close()方法断开连接。
2.创建EchoClient
在EchoClient程序中,为了与EchoServer通信,需要先创建一个Socket对象:
String host="localhost";
String port=8000;
Socket socket=new Socket(host,port);
在以上Socket的构造方法中,参数host表示EchoServer进程所在的主机的名字,参数port表示EchoServer进程监听的端口。当参数host的取值为“localhost”,表示EchoClient与EchoServer进程运行在同一个主机上。如果Socket对象成功创建,就表示建立了EchoClient与EchoServer之间的连接。接下来,EchoClient从Socket对象中得到了输出流和输入流,就能与EchoServer交换数据。
EchoClient的核心代码如下:
import java.net.*;
import java.io.*;
import java.util.*;
public class EchoClient {
private String host="localhost";
private int port=8000;
private Socket socket;
public EchoClient()throws IOException{
socket=new Socket(host,port);
}
public static void main(String args[])throws IOException{
new EchoClient().talk();
}
private PrintWriter getWriter(Socket socket)throws IOException{
OutputStream socketOut = socket.getOutputStream();
return new PrintWriter(socketOut,true);
}
private BufferedReader getReader(Socket socket)throws IOException{
InputStream socketIn = socket.getInputStream();
return new BufferedReader(new InputStreamReader(socketIn));
}
public void talk()throws IOException {
try{
BufferedReader br=getReader(socket);
PrintWriter pw=getWriter(socket);
BufferedReader localReader=new BufferedReader(new InputStreamReader(System.in));
String msg=null;
while((msg=localReader.readLine())!=null){
pw.println(msg);
System.out.println(br.readLine());
if(msg.equals("bye"))
break;
}
}catch(IOException e){
e.printStackTrace();
}finally{
try{socket.close();}catch(IOException e){e.printStackTrace();}
}
}
}
在EchoClient类中,最主要的方法为talk()方法。该方法不断读取用户从控制台输入的字符串,然后把它发送给EchoServer,再把EchoServer返回的字符串打印到控制台。如果用户输入的字符串为“bye”,就会结束与EchoServer的通信,调用socket.close()方法断开连接。
运行范例时,需要打开两个DOS界面,先在一个DOS界面中运行“java EchoServer”命令,再在另一个DOS界面中运行“java EchoClient”命令。图19显示了运行这两个程序的DOS界面。在EchoClient控制台,用户输入字符串“hi”,程序就会输出“echo:hi”。

图19 运行EchoServer和EchoClient程序
在EchoServer程序的service()方法中,每当serverSocket.accept()方法返回一个Socket对象,就表示建立了与一个客户的连接,这个Socket对象中包含了客户的地址和端口信息,只需调用Socket对象的。getInetAddress()和getPort()方法就能分别获得这些信息:
socket = serverSocket.accept(); //等待客户连接
System.out.println("New connection accepted "
+socket.getInetAddress() + ":" +socket.getPort());
从图19可以看出,EchoServer的控制台显示EchoClient的IP地址为127.0.0.1,端口为1874。127.0.0.1是本地主机的IP地址,表明EchoClient与EchoServer在同一个主机上。EchoClient作为客户程序,它的端口是由操作系统随机产生的。每当客户程序创建一个Socket对象,操作系统就会为客户分配一个端口。假定在客户程序中先后创建了两Socket对象,这意味着客户与服务器之间同时建立了两个连接:
Socket socket1=new Socket(host,port);
Socket socket2=new Socket(host,port);
操作系统为客户的每个连接分配一个唯一的端口,参见图20。

图20 客户与服务器进程之间同时建立了两个连接
在客户进程中,Socket对象包含了本地以及对方服务器进程的地址和端口信息,在服务器进程中,Socket对象也包含了本地以及对方客户进程的地址和端口信息。客户进程允许建立多个连接,每个连接都有唯一的端口。在图20中,客户进程占用两个端口:1874和1875。在编写网络程序时,一般只需要显式地为服务器程序中的ServerSocket设置端口,而不必考虑客户程序所用的端口。
六、结语
计算机网络的任务就是传输数据。为了完成这一复杂的任务,国际标准化组织ISO提供了OSI参考模型,这种模型把互联网络分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。每个层有明确的分工,并且在层与层之间,下层为上层提供服务。这种分层的思想简化了网络系统的设计过程。例如在设计应用层时,只需考虑如何创建满足用户实际需求的应用,在设计传输层时,只需考虑如何在两个主机之间传输数据,在设计网络层时,只需考虑如何在网络上找到一条发送数据的路径,即路由。
由于OSI参考模型过于庞大和复杂,使它难以投入到实际运用中。与OSI参考模型相似的TCP/IP参考模型吸取了网络分层的思想,但是对网络的层次做了简化,并且在网络各层(除了主机-网络层外)都提供了完善的协议,这些协议构成了TCP/IP协议集,简称TCP/IP协议。TCP/IP参考模型分为四个层:应用层、传输层、网络互联层和主机-网络层。在每一层都有相应的协议,IP协议和TCP协议是协议集中最核心的两个协议。
IP协议位于网络互联层,用IP地址来标识网络上的各个主机,IP协议把数据分为若干数据包,然后为这些数据包确定合适的路由。路由就是指把数据包从源主机发送到目标主机的路经。
TCP协议位于传输层,保证两个进程之间可靠地传输数据。每当两个进程之间进行通信,就会建立一个TCP连接,TCP协议用端口来标识TCP连接的两个端点。在传输层还有一个UDP协议,它与TCP协议的区别是,UDP不保证可靠地传输数据。
建立在TCP/IP协议基础上的网络程序一般都采用客户/服务器通信模式。服务器程序提供服务,客户程序请求获得服务。服务器程序一般昼夜运行,时刻等待客户的请求并及时做出响应。 Java网络程序致力于实现应用层。传输层向应用层提供了套接字Socket接口,Socket封装了下层的数据传输细节,应用层的程序通过Socket来建立与远程主机的连接以及进行数据传输。在Java中,有三种套接字类:java.net.Socket、java.net.ServerSocket和DatagramSocket。其中Socket和ServerSocket类建立在TCP协议基础上;DatagramSocket类建立在UDP协议基础上。
|