概述
就像建筑物有地址一样,在网络中的主机要和其它主机通信需要IP(Internet Protocol)地址,IP地址用来在网络中标志一台主机。IP地址由32位二进制组成,为了方便阅读,一般都表示为4个0到255的十进制数,比如192.168.0.1。尽管写成十进制数方便阅读,但仍然不好记忆,于是人们用域名来对应地址,比如baidu.com,这就像建筑的名字和地址的关系一样。当用主机的域名来访问它时,需要将域名翻译为主机的IP地址,这需要域名服务器(domain name server)的协助。
IP协议是在网络中的主机之间传递数据的较底层的协议,在IP协议中数据都是以分组报文的形式,携带目标主机的IP地址由路由器向目标主机发送,这就像包裹通过邮政系统向某个地方发送一样,如果有大量的数据要发送,需要拆成多个小型的分组报文。IP协议只是一个“尽力而为”的协议:它试图分发每一个分组报文,但在网络传输过程中,可能会丢失报文,报文顺序被打乱等情况。
因此就有了在IP协议之上的两个较高超的协议,TCP协议和UDP协议。这两个协议都是在IP协议提供的服务基础上工作的。TCP协议提供的服务可以想象成在两个终端之间建立了稳定的数据流通道,对两台主机来说,TCP协议让它们可以进行数据的相互输入、输出数据,TCP协议保证数据不丢失、以及顺序到达等特性。UDP协议并不会做这些,它仅仅是简单的扩展了IP协议“尽力而为”的数据报服务,并不保证数据的不丢失,达到的顺序等特性。
TCP协议也被称为基于流的协议,UDP协议也被称为是基于包的协议。下面的讨论以流式的方式在主机之间传递数据。
客户端/服务端编程模型
Java提供了ServerSocket类来协助创建一个服务端socket,以及Socket类来创建一个客户端socket,在这两个socket之间用I/O流的方式在两个程序之间传递数据。socket可以看成服务器和客户端逻辑连接的两个端点,每个socket都绑定了两条数据传输的通道,一个输入流和一个输出流,一端的输入流与另一端的输出流相连,往socket中读写数据就像往文件中读写数据一样。
服务端socket
服务器上socket的建立需要ServerSocket类的协助。
在服务器的某个端口上(端口号用来区分同一主机上的不同应用程序),比如8000,创建ServerSocket。
ServerSocket server = new ServerSocket(8000);
ServerSocket在这个端口上监听连接请求,一旦有连接请求,就会返回一个socket
Socket socket = server.accept();
server.accept()
会一直等待(阻塞),直到一个客户端连接上了服务端
端口号
在一台主机上可能不只有一个应用程序,那么只用网络地址标志应用程序是不够的,这就引出了端口号的概念,端口号是用来区分同一主机中的不同应用程序的。TCP协议和UDP协议也称为端到端传输协议(end to end transport protocal),因为它们将数据从一个应用程序传输到另一个应用程序,而IP协议只是将数据从一个主机传输到另一主机。端口号是一组16位的无符号二进制数,每个端口号的范围是1~65535(0被保留),1~1024被用于常见的应用,比如邮箱服务运行在端口25,Web服务运行在端口80。
创建ServerSocket时要选择一个没有被使用的端口
ServerSocket server = new ServerSocket(port);
如果端口正在被其它程序所占用,那么上述代码会抛出java.net.BindException异常。
客户端socket
在客户端只要创建一个Socket就可以尝试与服务端连接,如果连接上,就会返回代表服务端的socket,
Socket socket = new Socket(serverName, port);
serverName是服务端在网络中的主机名或者IP地址。下面的代码要连接的IP地址为130.254.204.33,端口是8000
Socket socket = new Socket(“115.239.210.27”, 8000);
或者,可以使用主机名来建立连接
Socket socket = new Socket("baid.com”, 8000);
如果提供的是主机域名,会请求DNS服务,将主机域名翻译成IP地址。
本机地址
可以用主机名localhost或者地址127.0.0.1来表示本机地址。客户端socket可以用它们来与运行在同一台主机上的服务端socket连接。
传输数据
在已经连接的两端socket上,可以引出两条数据流,服务端的输入流连接着客户端的输出流,服务端的输出流连接着客户端的输入流,可以像读写文件那样操作这两个流。
一个例子
编写客户端向服务器发送数据。服务器接收数据,使用它产生结果,然后将结果发送回客户端。客户端将结果显示在控制台上。在这个例子中,从客户端发送的数据是圆的半径,服务器产生的结果是圆的面积。
Server端代码
import java.io.*;
import java.net.*;
import java.util.Date;
import javax.swing.*;
public class Server{
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Server();
}
});
}
Server(){
JFrame jfrm = new JFrame("Server");
jfrm.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JTextArea jta = new JTextArea();
jfrm.getContentPane().add(jta);
jfrm.setSize(320, 240);
jfrm.setVisible(true);
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
ServerSocket serverSocket = new ServerSocket(8000);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
jta.append("Server started at " + new Date() + "\n");
}
});
Socket socket = serverSocket.accept();
// Create data input and output streams
DataInputStream inputFromClient = new DataInputStream(
socket.getInputStream());
DataOutputStream outputToClient = new DataOutputStream(
socket.getOutputStream());
while (true) {
// Receive radius from the client
double radius = inputFromClient.readDouble();
// Compute area
double area = radius * radius * Math.PI;
// Send area back to the client
outputToClient.writeDouble(area);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
jta.append("Radius received from client: "
+ radius + '\n');
jta.append("Area is: " + area + '\n');
}
});
}
}
catch(IOException ex) {
ex.printStackTrace();
}
}
}).start();
}
}
Client端代码
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.awt.*;
import javax.swing.*;
public class Client {
// IO streams
DataOutputStream toServer = null;
DataInputStream fromServer = null;
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new Client();
}
});
}
Client(){
JFrame jfrm = new JFrame("Client");
JPanel jp = new JPanel();
jp.setLayout(new FlowLayout(FlowLayout.LEFT));
jp.add(new JLabel("Radius:"));
JTextField jtf = new JTextField(10);
jp.add(jtf);
JTextArea jta = new JTextArea();
jtf.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
// TODO Auto-generated method stub
try {
String str = jtf.getText();
Double radius = Double.parseDouble(str);
toServer.writeDouble(radius);
double area = fromServer.readDouble();
jta.append("radius is " + radius + "\n");
jta.append("area is " + area + "\n");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
jfrm.getContentPane().add(jta, BorderLayout.CENTER);
jfrm.getContentPane().add(jp,BorderLayout.NORTH);
jfrm.setSize(320, 240);
jfrm.setVisible(true);
try {
Socket socket = new Socket("localhost", 8000);
// Create an input stream to receive data from the server
fromServer = new DataInputStream(socket.getInputStream());
// Create an output stream to send data to the server
toServer = new DataOutputStream(socket.getOutputStream());
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
因为服务端需要为客户端持续不断的服务,服务端是以循环来实现这个功能,因此服务端需要在线程中完成网络交互的代码。
在非事件派发线程中要与界面元素进行交互,需要使用SwingUtilities.invokeLater()
代码将操作转移到事件派发线程中运行。
InetAddress类
如果想知道与服务端相连的客户端的主机名和IP地址,可以使用 InetAddress类。这个类代表一个IP地址。在服务端程序里,可以使用下面的语句获取与服务端相连的客户端的地址。
InetAddress inetAddress = socket.getInetAddress();
接下来,可以得到主机名和地址,
System.out.println("Client's host name is " + inetAddress.getHostName());
System.out.println("Client's host address is " + inetAddress.getHostAddress());
InetAddress的类方法getByName()可以根据域名构造一个InetAddress,进而可以获取到该域名的IP地址,
InetAddress address = InetAddress.getByName("baidu.com");
System.out.println("host name is " + inetAddress.getHostName());
System.out.println("host address is " + inetAddress.getHostAddress());
多客户端
一般服务端/客户端编程方式是服务端为多个客户服务,而如果有多个客户端近乎同时访问服务端的话,服务端如果只用一个线程来进行连接和处理数据的话会造成很多客户端阻塞。因此可以借助于线程来处理数据。这样服务端就可以“同时”处理很多连接。例如,服务端的关键代码可以像下面这样,
while (true) {
Socket socket = serverSocket.accept(); // Connect to a client
Thread thread = new ThreadClass(socket);
thread.start();
}
每次循环都会产生一个新的连接,每次连接产生之后,都用一个新的线程来处理这个连接上的数据处理。
import java.io.*;
import java.net.*;
import java.util.Date;
import javax.swing.*;
public class MultiThreadServer {
JTextArea jta;
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new MultiThreadServer();
}
});
}
MultiThreadServer() {
JFrame jfrm = new JFrame("Server");
jfrm.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jta = new JTextArea();
jfrm.getContentPane().add(jta);
jfrm.setSize(320, 240);
jfrm.setVisible(true);
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
ServerSocket serverSocket = new ServerSocket(8000);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
jta.append("Server started at " + new Date() + "\n");
}
});
while (true) {
Socket socket = serverSocket.accept();
System.out.println("Client connected");
// Create data input and output streams
new Thread(new HandleAClient(socket)).start();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}).start();
}
class HandleAClient implements Runnable {
private Socket socket;
HandleAClient(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// TODO Auto-generated method stub
DataInputStream inputFromClient = null;
DataOutputStream outputToClient = null;
try {
inputFromClient = new DataInputStream(socket.getInputStream());
outputToClient = new DataOutputStream(socket.getOutputStream());
while (true) {
// Receive radius from the client
double radius = inputFromClient.readDouble();
// Compute area
double area = radius * radius * Math.PI;
// Send area back to the client
outputToClient.writeDouble(area);
SwingUtilities.invokeLater(new Runnable() {
public void run() {
jta.append("Radius received from client: " + radius + '\n');
jta.append("Area is: " + area + '\n');
}
});
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
参考说明
本文基本翻译自《Introduction to Java Programming》第10版,第31章。仅供教学目的,如有侵权,请联系。