第14章Java网络编程技术
网络编程的目的就是直接或间接地通过网络协议与其他计算机进行通信。网络编程中有两个主要的问题:一个是如何准确地定位网络上一台或多台主机,另一个就是找到主机后如何可靠、高效地进行数据传输。
在TCP/IP协议集中,IP协议主要负责网络主机的定位,由IP地址可以唯一地确定Internet上的一台主机;而TCP协议实现可靠的数据传输。
目前较为流行的网络编程模型是客户机/服务器(C/S)模式,即通信的一方作为服务器等待客户机提出请求并予以响应;客户机则在需要服务时向服务器提出申请。服务器一般作为守护进程始终运行,监听服务器的网络端口,一旦某个客户机提出请求,服务器就会为每个客户机创建并启动一个服务线程来响应该客户机,同时自己继续监听服务器的网络端口,使后来的客户机也能及时得到服务。
14.1 使用URL
URL(Uniform Resource Locator,统一资源定位器)主要用来对Internet上的信息进行定位。浏览器借助它来查找Web上的信息。实际上,Web通过URL和HTML 对所有的网络资源定位。在Java的网络编程中,程序员使用URL类库,获取Internet 上的信息。
14.1.1 URL组成
URL的两种语句格式:
1.http://www.baidu.com/
2.http://www.baidu.com:80/index.htm
从上面的URL格式可以看出,URL由以下4个部分组成:
(1) 协议(http)。指网络连接用到的协议,如,本例的http是超文本协议,http不是唯一的协议,常见的协议还有 FTP、Gopher等。其中的冒号:将协议与URL的其他部分隔开。
(2) 主机名或其IP地址(www.baidu.com)。该项位于双斜线(//)和单斜线(/)之间,其中的冒号(:)是可选部。如,本例的/www.baidu.com或www.baidu.com:80表示了主机名。
(3) 端口号(80)。它是可选的参数,它位于主机名和右边的单斜线(/)之间(HTTP协议的默认端口为80,所以“:80”可以不写),可以是别的数字组成端口号。
(4) 网页文件及所在的目录(index.htm)。网页文件及目录是浏览器要查找的网页资源。如index.html或index.htm文件。
14.1.2 URL类
在Java中URL类的继承关系如下:
public final class URL extends Object implements Serializable
1.URL的构造方法
URL类有多个构造方法,它们都可能抛出MalformedURLException异常。
(1)URL(String urlSpecifier) 的使用格式
URL url=new URL("http://www.sina.com/");
(2)URL(String protocol, String hostName, int port, String path) 的使用格式
URL url=new URL("http","sports.163.com",80,"nba");
(3)URL(String protocol, String hostName, String path) 的使用格式
URL url=new URL("http","sports.163.com","nba");
(4)URL(URL context, String spec) 使用一个已经存在的URL创建一个新的URL
URL url1=new URL("http://sprots.163.com/"); //构造url1对象
URL url2=new URL(url,"nba"); //利用上面的url1对象和一个字符串为参数创建新的url2对象
2.URL的实用方法
l boolean equals(Object obj):比较两个URL是否相同。
l String getAuthority():获得URL的授权部分。
l Object getContent():获得URL的内容。
l Object getContent(Class[] classes):获得URL的内容。
l int getDefaultPort():获得URL中的默认端口号。
l String getFile():获得URL中的网页文件名。
l String getHost():获得URL中的主机名。
l String getPath():获得URL中网页所在的路径。
l int getPort():获得URL中的端口号,若没有设置端口号则返回-1。
l String getProtocol():获得URL所用的协议名称。
l String getQuery():获得URL的查询部分。
l String getRef():获得URL的锚点(也称为“引用”)。
l String getUserInfo():获得URL的UserInfo部分。
l int hashCode():创建一个适合散列表索引的整数。
l InputStream openStream():打开URL的链接。并返回一个用于从该链接读入的InputStream流。
l boolean sameFile(URL other):比较两个URL,不包括片段部分。
l String toExternalForm():将URL转换为字符串格式。
l String toString():构造URL的字符串表示形式。
l URI toURI():返回与URL等效的URI。
3.URL应用
【例14.1】创建一个URL实例,然后检查它的相关属性。
程序名:Example14_1.java。
【程序源代码】
import java.net.*;
class Example14_1 {
public static void main(String args[]) throws MalformedURLException {
try {
URL url = new URL("http://www.baidu.com"); //创建一个URL对象
System.out.println("授权: " + url.getAuthority()); //检查它的授权部分
System.out.println("协议名称: " + url.getProtocol()); //获得此 URL 的协议名
System.out.println("默认端口号: " + url.getDefaultPort()); //获得默认端口号
System.out.println("主机名: " + url.getHost()); //获得此 URL 的主机名
System.out.println("文件名: " + url.getFile()); //获得此 URL 的文件名
System.out.println("Ext: " + url.toExternalForm());
System.out.println("字符串表示形式: " + url.toString());
}
catch (MalformedURLException ex) { System.out.println("fail !"); }
}
}
【例14.2】在Applet的文本框中输入一个网址,单击search(搜索)按钮后链接到该网址相对应的页面。
程序名:Example14_2.java
【程序说明】要在Applet中链接到其它Web页面,可使用下面的代码来实现。
getAppletContext().showDocument(url); //执行该方法,转向url页面
因为getAppletContext()方法是在 Applet 类中定义的,由showDocument()定位到另一个Web页面。
【程序源代码】
import java.awt.*; import java.awt.event.*; import javax.swing.*;
import java.net.*; import java.io.*; import java.applet.*;
public class ShowWebPage extends JApplet implements ActionListener {
private static final long serialVersionUID = 1L;
private JLabel lfn = new JLabel("URL address ");
private JTextField tfu = new JTextField(20);//网址输入框
private JButton searb = new JButton("search");//搜索按钮
/** 初始化界面 */
public void init() {
Container c = getContentPane();//获取底层容器
searb.addActionListener(this);//为按钮注册监听器
c.setLayout(new FlowLayout());//设置布局
c.add(lfn); // 添加标签
c.add(tfu); //添加网址输入文本框
c.add(searb); //添加Search(搜索)按钮
}
/** 响应按钮事件 */
public void actionPerformed(ActionEvent e) {
String s = tfu.getText(); //获取输入框内容
try {
URL u = new URL(s); //利用网址输入框中的内容创建一个URL对象
getAppletContext().showDocument(u);//显示指定URL的网页
}
catch (MalformedURLException ex) { showStatus(s + "file format error!");}
}
}
14.2 Socket套接字
14.2.1 Socket的含义
Socket描述了网络上运行的两个程序间实现通信的任一端。它既可以接收请求,也可以发送请求。利用它可以方便地编写网络上传递数据的程序。在Java中,有专门的Socket类来处理用户的请求和响应。利用Socket类的方法,就可以实现两台计算机之间的通信。
在Java中,Socket可以理解为客户端或者服务器端的一个特殊的对象。这个对象有两个关键的方法:一个是getInputStream()方法;另一个是getOutputStream()方法。getInputStream()方法可以得到一个输入流。客户端的Socket对象上的getInputStream()方法得到的输入流就是从服务器端发送回来的数据流。getOutputStream()方法得到一个输出流。客户端的Socket对象上的getOutputStream()方法返回的输出流就是将要发送到服务器端的数据流(其实是一个缓冲区,暂时存储将要发送到服务器端的数据)。
下面介绍Socket实现网络通信的机制。
先来看看服务器是如何与客户机进行通信的。每当客户机要求与服务器通信时,在服务器端,服务器套接字便会为客户机创建一个客户机的代理套接字。通过代理套接字,服务器与客户端的客户机套接字建立一个连接。通过这个连接,服务器便可与客户机进行通信。其通信方式如图14-1所示。
图14-1 服务器与客户机进行通信的方式
1.建立服务器端套接字
Java中有一个ServerSocket类,专门用来建立服务器端的套接字。可以用服务器需要使用的端口号作为参数来创建服务器套接字。
ServerSocket server = new ServerSocket(8000);
这条语句创建了一个服务器套接字server。该服务器使用8000号端口监听客户机请求。
2.在服务器端为每个客户机建立代理套接字
当一个客户端程序要求与服务器建立一个Socket连接时,在服务器端,服务器套接字便会为客户机建立一个客户机代理套接字connectToClient,通过该套接字与客户机端的Socket套接字建立连接。
(1)在服务器端为客户机建立代理套接字的格式如下:
Socket connectToClient=server.accept() ; //代理套接字connectToClient
(2)服务器端从客户端收到的输入流s_in(也就是客户端的输出流)的其格式如下:
BufferedReader s_in = new BufferedReader(new InputStreamReader(connectToClient.getInputStream()));
(3)服务器端传给客户端的输出流s_out(也就是客户端的输入流)的格式如下:
PrintWriter out = new PrintWriter(connectToClient.getOutputStream(),true);
随后,就可以使用s_in.readLine()方法得到客户端的输入;也可以使用s_out.println()方法向客户端发送数据。
在所有通信结束以后应该关闭这两个数据流。关闭的顺序是先关闭输出流,再关闭输入流,即依次调用out.close(); 和in.close(); 方法。
3.建立客户端套接字(Socket)
客户端只需用服务器所在机器的IP以及服务器的端口号作为参数即可创建一个客户端套接字(connectToServer)。假设服务器的端口号是8000。
(1)建立客户机的套接字格式如下:
Socket connectToServer = new Socket("服务器IP地址",8000);
或者
Socket connectToServer = new Socket("服务器主机名",8000);
(2)客户端从服务器端收到的输入流in(也就是服务器端的输出流)的格式如下:
InputStream in = connectToServer.getInputStream();
(3)客户端向服务器端发送的输出流out(也就是服务器端的输入流)的格式如下:
OutputStream out = connectToServer.getOutputStream();
14.2.2 Socket的应用
【例14.3】实现客户机和服务器相互交替接收对方所写入的信息,实现两者间的通信。
程序名:MyClient.java && MyServer.java
【程序源代码】
/**客户端程序:MyClient.Java*/
package socket; import java.net.*; import java.io.*;
public class MyClient {
static Socket server;
public static void main(String[] args) throws IOException {
try {
/** 向本机的 5678 端口发出客户机请求
*由Socket 对象得到输入流,并构造相应的 BufferedReader 对象
*由Socket 对象得到输出流,并构造 PrintWriter 对象
*/
server = new Socket(InetAddress.getLocalHost(), 5678);
BufferedReader in=new BufferedReader(new InputStreamReader(server.getInputStream()));
PrintWriter out = new PrintWriter(server.getOutputStream());
/** 由系统标准输入设备构造 BufferedReader 对象作为用户输入信息流*/
System.out.println("输入发送信息:");
BufferedReader wt=new BufferedReader(new InputStreamReader(System.in));
/** 利用循环不断发送数据*/
while (true) {
String str = wt.readLine();//从系统标准输入设备读入一字符串
out.println(str); //将从系统标准输入设备读入的字符串输出到 Server
out.flush(); //刷新输出流,使 Server 马上收到该字符串
if (str.equals("end")) { break; }
System.out.println("Server说:" + in.readLine());//输出接收到信息
}
out.close(); //关闭 Socket 输出流
in.close(); //关闭 Socket 输入流
server.close(); //关闭 Socket
}
catch (Exception e) { System.out.println("error: " + e); }
}
}
/**服务器端程序:MyServer.java*/
package socket; //类包
import java.io.*; import java.net.*;
public class MyServer {
public static void main(String[] args) throws IOException {
try {
//创建一个 ServerSocket 在端口 5678 监听客户机请求
ServerSocket server = new ServerSocket(5678);
/**使用accept()阻塞等待客户机请求,
*有客户机请求到来则产生一个 Socket 对象,并继续执行
*/
Socket client = server.accept();
//显示连接到的客户机IP
System.out.println("connect to :" + client.getInetAddress());
//由系统标准输入设备构造 BufferedReader 对象作为用户输入信息流
BufferedReader wt = new BufferedReader(new InputStreamReader(System.in));
//由Socket对象得到输入流,并构造相应的BufferedReader对象
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
//由Socket对象得到输出流,并构造PrintWriter对象
PrintWriter out = new PrintWriter(client.getOutputStream());
/** 利用循环不断接收数据*/
while (true) {
String str1 = in.readLine();
//在标准输出设备上打印从客户端读入的字符串
System.out.println("Client说:" + str1);
if (str1.equals("end")) break; //若客户机输入"end",则断开与客户机的连接
String str2 = wt.readLine();
out.println(str2); //将从系统标准输入设备读入的字符串输出到客户机
out.flush(); //刷新输出流,使服务器马上收到该字符串
}
out.close(); //关闭 Socket 输出流
in.close(); //关闭 Socket 输入流
client.close(); //关闭 Socket
server.close(); //关闭 ServerSocket
}
catch (Exception e) { System.out.println("error: " + e); }
}
}
【程序运行结果】
将上面两个程序放在Socket目录下编译,先运行服务器端,然后运行客户端,结果如图14-2、图14-3所示。
图14-2 例14.3客户端程序运行结果
图14-3 例14.3服务器端程序运行结果
【例14.4】Socket的多线程通信。
程序名:Cal_Client.java && Cal_Server.java
【程序说明】在本例中,在客户机输入三角形3条边的长度并发送给服务器;服务器把计算出的三角形面积返回给客户机。可以将计算量大的工作放在服务器端,客户端则负责计算量小的工作,实现客户机、服务器交互计算,从而完成任务。
套接字连接中涉及到输入流和输出流操作,为了不影响做其他的事情,应把套接字连接放在一个单独的线程中。另外,服务器端为每个客户机创建并启动一个服务线程。
【程序源代码】
/**客户端代码:Cal_Client.Java**/
import java.net.*; import java.io.*; import java.awt.*;
import java.awt.event.*; import java.applet.*; import javax.swing.*;
public class Cal_Client extends JApplet implements Runnable, ActionListener
{
JButton b_calc; //计算按钮
TextField b_edge, b_result; // b_edg是 3条边输入的文本框; b_result是结果文本框
Socket socket = null; //连接套接字
DataInputStream in = null; //输入数据流
DataOutputStream out = null; //输出数据流
Thread thread; //负责接收计算结果的线程
/** 下面初始化界面*/
public void init()
{
setLayout(new GridLayout(2, 2));
JPanel p1 = new JPanel(), p2 = new JPanel();
b_calc = new JButton(" b_calc");
b_edge = new TextField(12); b_result = new TextField(12);
p1.add(new JLabel("输入三角形三边的长度,用逗号或空格分隔:"));
p1.add(b_edge); p2.add(new JLabel("计算结果 :"));
p2.add(b_result); p2.add(b_calc);
b_calc.addActionListener(this);
add(p1); add(p2);
/** 创建连接套接字,并构造相应的输入输出流对象*/
Try
{
/**向本机的 4331 端口发出客户请求
*由 Socket 对象得到输入流,并构造相应的 DataInputStream 对象
*由 Socket 对象得到输出流,并构造 DataOutputStream 对象
*/
socket = new Socket("localhost", 4331);
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
}
catch (IOException e) {}
/** 创建线程,负责接收服务器信息*/
if (thread == null) { thread = new Thread(this); thread.start(); }
}
/** 线程调用方法,负责接收运算结果*/
public void run()
{ String s = null;
while (true)
{
try {
// 利用 Socket 输入流对象接收服务器运算结果,在读取到信息之前处于堵塞状态。
s = in.readUTF();
b_result.setText(s);
}
catch (IOException e) { b_result.setText("与服务器已断开"); break; }
}
}
/** 响应动作事件,负责将数据发送到服务器端*/
public void actionPerformed(ActionEvent e)
{ if (e.getSource() == b_calc)
{
String s = b_edge.getText();
if (s != null)
{ try
{
// 将三角形的边长数据通过 Socket 输出流对象发送到服务器
out.writeUTF(s);
}
catch (IOException e1) {}
}
}
}
}
/**对应的HTML文档:
*<APPLET CODE= Cal_Client.class WIDTH=500 HEIGHT=500>
*</APPLET>
*/
/**服务器端代码: Cal_Server.java**/
import java.io.*; import java.net.*; import java.util.*;
public class Cal_Server
{
public static void main(String args[])
{
ServerSocket server = null;
Server_thread thread;
Socket you = null;
while (true)
{
try
{
server = new ServerSocket(4331);
}
catch (IOException e1) { System.out.println("正在监听"); //ServerSocket对象不能重复创建}
try
{
you = server.accept();
System.out.println("客户的地址:" + you.getInetAddress());
}
catch (IOException e) {System.out.println("正在等待客户"); }
if (you != null) {new Server_thread(you).start();//为每个客户启动一个专门的线程 }
else {continue; }
}
}
}
/** 线程类,负责计算面积工作* */
class Server_thread extends Thread
{
Socket socket = null; //连接套接字
DataInputStream in = null; //数据输入流
DataOutputStream out = null; //数据输出流
String s = null; //待发送的运算结果
/** 利用Cal_Server类中的Socket对象构造线程对象* */
public Server_thread(Socket t)
{
socket = t;
try
{
// 由 Socket 对象得到输入流,并构造相应的 DataInputStream 对象
in = new DataInputStream(socket.getInputStream());
// 由Socket对象得到输出流,并构造 DataOutputStream 对象
out = new DataOutputStream(socket.getOutputStream());
}
catch (IOException e) { }
}
/** 线程调用方法,负责计算面积,并将计算结果发给客户端* */
public void run()
{
while (true)
{
double a[] = new double[3];
int i = 0;
try { /** 利用Socket输入流对象接收客户端传来的边长参数,
*在读取到信息之前处于堵塞状态
*/
s = in.readUTF();
/** 判断、分析传来的参数,若格式错误则向客户端提示错误,
*正确则进行面积计算并返回给客户端
*/
StringTokenizer fenxi = new StringTokenizer(s, " ,");
while (fenxi.hasMoreTokens())
{
String temp = fenxi.nextToken();
try
{
a[i] = Double.valueOf(temp).doubleValue();
i++;
}
catch (NumberFormatException e) { out.writeUTF("请输入数字字符"); }
}
double p = (a[0] + a[1] + a[2]) / 2.0;
/**将面积通过Socket输出流对象发送给客户端*/
out.writeUTF(" "
+ Math.sqrt(p * (p - a[0]) * (p - a[1]) * (p - a[2])));
sleep(2);
}
catch (InterruptedException e) { }
catch (IOException e) { System.out.println("客户离开"); break; }
}//while语句结束
}//方法结束
}
【程序运行结果】
程序运行结果如图14-4、图14-5所示。
图14-4 例14.4客户端程序运行结果 图14-5 例14.4服务器端程序运行结果
14.3 InetAddress类
java.net.InetAddress类封装了IP地址。该类的声明格式如下:
public final class InetAddress extends object implements Serializable
该类里有两个成员变量:hostName(数据类型是String)和address(数据类型是int),即主机名和IP地址。这两个成员变量是的访问权限是私有的(private)。
14.3.1 InetAddress类
1.获取InetAddress对象
InetAddress类没有构造方法。可以通过该类的类方法获取其实例。
(1)public static InetAddress getLocalHost()
该方法获得本地机器的InetAddress对象,当查找不到本地机器的地址时将抛出一个UnknownHostException异常。示范代码如下:
try {
InetAddress address=InetAddress.getLocalHost( );
… //其他处理代码
}
catch(UnknownException e) {
… //异常处理代码
}
(2)public static InetAddress getByName (String host)
该方法获得host(计算机的域名)指定的InetAddress对象。如果找不到主机将触发UnknownHostException异常。示范代码如下:
try {
InetAddress address=InetAddress.getByName( host );
… //其他处理代码
}
catch(UnknownException e) {
… //异常处理代码
}
(3)public static InetAddress[] getAllByName(String host)
该方法把网络上的一组计算机的InetAddress对象保存在数组中。出错了同样会抛出UnknownException异常。示范代码如下:
try {
InetAddress address=InetAddress.getAllByName( host );
… //其他处理代码
}
catch(UnknownException e) {
… //异常处理代码
}
提示:InteAddress类有一个getAddress()方法,该方法将IP地址以网络字节顺序作为字节数组返回。当前IP只有 4个字节,但是当实行IPV6时就有6个字节了。如果需要知道数组的长度,可以使用数组的length字段。getAddress( )方法的一般性用法如下:
InetAddress inetaddress=InetAddress.getLocalHost( );
byte[ ] address=inetaddress.getAddress( );
2.InetAddress类的实用方法
(1)public String getHostName()
该方法返回主机名(一个字符串)。如果要查询的机器没有主机名,则该方法就返回主机的IP地址。一般的使用格式如下:
InetAddress inetadd = InetAddress.getLocalHost( );
String localname= inetadd.getHostName( );
(2)public String getHostAddress()
该方法返回主机的 IP 地址(字符串格式)。
(3)public String toString()
该方法返回主机名和IP地址(字符串格式),其具体形式为“主机名/点分地址”。如果一个InetAddress对象没有主机名,则返回IP地址(字符串格式)。
14.3.2 InetAddress类的应用
【例14.5】查询IP地址是IPV4还是IPV6,以及地址的类型(A,B,C,D)。
程序名:Example14_5.java
【程序源代码】
import java.net.*; import java.io.*;
public class Example14_5 {
public static void main(String args[]) {
try { //获得本地IP地址
InetAddress inetadd = InetAddress.getLocalHost();
//将IP地址以网络字节顺序作为字节数组返回
byte[] address = inetadd.getAddress();
if (address.length == 4) {
System.out.println("The ip version is ipv4");
int firstbyte = address[0];
/**
* 由于返回的byte[ ]字节是无符号的,但是Java没有无符号字节的基本数
* 据类型,因此如果要对返回的字节进行操作,必须要将int进行适当的调整
*/
if (firstbyte < 0)
firstbyte += 256;
if ((firstbyte & 0x80) == 0) // firstbyte<=126,对应IP为A类地址
System.out.println("the ip class is A");
// 128<=firstbyte<=191,对应IP为B类地址
else if ((firstbyte & 0xC0) == 0x80)
System.out.println("The ip class is B");
// 192<=firstbyte<=223,对应IP为C类地址
else if ((firstbyte & 0xE0) == 0xC0)
System.out.println("The ip class is C");
// 224<=firstbyte<=239,对应IP为D类地址
else if ((firstbyte & 0xF0) == 0xE0)
System.out.println("The ip class is D");
// 240<=firstbyte<=255,对应IP为E类地址
else if ((firstbyte & 0xF8) == 0xF0)
System.out.println("The ip class is E");
}
else if (address.length == 16)
System.out.println("The ip version is ipv6");
}
catch (Exception e) {}
}
}
【程序运行结果】
The ip version is ipv4
The ip class is C
【例14.6】获取域名是www.baidu.com和www.sina.com.cn的主机域名及ip地址
程序名:Example14_6
【程序源代码】
import java.net.*;
public class Example14_6
{
public static void main(String args[])
{
try
{ InetAddress address_1=InetAddress.getByName("www.baidu.com");
System.out.println(address_1.toString());
InetAddress address_2=InetAddress.getByName("www.sina.com.cn");
System.out.println(address_2.toString());
}
catch(Exception e){ System.out.println("无法找到 www.baidu.com"); }
}
}
14.4 UDP数据报
在TCP/IP协议包含TCP协议和UDP协议。相对而言,UDP的应用不如TCP广泛,几个标准的应用层协议HTTP、FTP、SMTP等使用的都是TCP协议。但是,随着计算机网络的发展,UDP协议正越来越显示出其威力,尤其是在需要很强的实时交互性的场合,如网络游戏、视频会议等,UDP更是显示出极强的威力。下面就来介绍Java环境下如何实现UDP网络传输。
14.4.1 什么是Datagram
所谓数据报(Datagram)就像日常生活中的邮件系统一样,不能确保可靠地寄到;而面向链接的TCP就好比是电话,双方能肯定对方接收到了信息。
TCP和UDP的区别:TCP可靠,传输大小无限制,但是需要时间建立连接,差错控制开销大;UDP不可靠,差错控制开销较小,传输大小限制在64KB以下,不需要建立 连接。
14.4.2 Datagram通信
java.net包中提供了两个类(DatagramSocket和DatagramPacket)支持数据报通信。其中,DatagramSocket用于在程序之间建立通信连接;DatagramPacket则用来表示一个数据报。
1. DatagramSocket的构造方法
l DatagramSocket(); //构造方法调用时都要抛出SocketException异常。
l DatagramSocket(int prot); 构造方法调用时都要抛出SocketException异常。
l DatagramSocket(int port, InetAddress laddr);。
其中,port指明Socket所使用的端口号,如果未指明端口号,则把Socket连接到本地主机上一个可用的端口;laddr指明一个可用的本地地址。给出端口号时要保证不发生端口冲突,否则会抛出SocketException异常。使用时的格式如下:
try {
DatagramSocket ds1=DatagramSocekt( );
DatagramSocekt ds2=DatagramSocket(5678);
DatagramSocekt ds3=DatagramSocket(5678, InetAddress.getByName(localhost).);
… //其他处理代码
}
catch(SocketException e) {
… //异常处理代码
}
用数据报方式编写Client/Server程序时,无论在客户端还是服务器端,首先都要建立一个DatagramSocket对象,用来接收或发送数据报,然后使用DatagramPacket对象作为传输数据的载体。
2.DatagramPacket的构造方法
l DatagramPacket(byte buf[],int length);。
l DatagramPacket(byte buf[], int length, InetAddress addr, int port);。
l DatagramPacket(byte[] buf, int offset, int length);。
l DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port);。
其中,buf中存放数据报数据,length为数据报中数据的长度,addr和port指明目的地址,offset指明了数据报的位移量。
3.基于UDP的通信模式
(1)将数据打包(称为数据包),就像将信件装入信封一样,然后将数据包发往目 的地。
在发送数据包前,先要创建一个DatagramPacket对象,这时就要用到上文介绍的构造方法Datagrampacker(byte buf[],int length,InetAddress addr,int port);在给出存放发送数据的缓冲区的同时,还要给出完整的目的地址,包括IP地址和端口号。发送数据是通过DatagramSocket的方法send()实现的。send()根据数据报的目的地址来寻径,以传递数据报。
发送数据报的格式如下:
try{
InetAddress address=InetAddress.getByName("localhost");
DatagramPacket data_pack=new DatagramPacket(buffer,buffer.length, address,888);
DatagramSocket mail_data=new DatagramSocket();
mail_data.send(data_pack);//利用send()方法将指定的数据从端口888处发送出去
… //其他处理代码
}
catch(Exception e)
{ //异常处理代码 }
(2)接收别人发来的数据包,好比接收信件一样,然后查看数据包中的内容。
在接收数据前,应该采用上面介绍的Datagrampacker(byte buf[],int length)方法创建一个DatagramPacket对象,给出接收数据的缓冲区及其长度。然后调用DatagramSocket的方法receive()等待数据报的到来。receive()将一直等待,直到收到一个数据报为止。
接收数据包的格式如下:
try{
DatagramSocket mail_data=new DatagramSocket(666); //从端口666处接收数据
DatagramPacket data_pack=new DatagramPacket(data,data.length);
//data为指定接收数据的字节数组
mail_data.receive(data_pack); //利用receive()方法等待接收数据
… //其他处理代码
}
catch(Exception e) { //异常处理代码 }
14.4.3 UDP数据报的应用
【例14.7】利用UDP实现网络通信。
程序名:UDP_ME.Java && UDP_YOU.java
【程序说明】本例通过UDP数据报实现网络通信。为了方便测试,本例的通信双方都设置在本地主机上(同一台机器)。读者可以根据实际情况调整DatagramPacket对象,实现在不同主机之间进行通信。
【程序源代码】
/** UDP_ME端代码:UDP_ME.Java */
package udp; //类包
import java.net.*; import java.io.*; import java.awt.*;
import java.awt.event.*; import javax.swing.*; import javax.swing.event.*;
public class UDP_ME extends JFrame {
public UDP_ME() {
Chat_me cs = new Chat_me();
getContentPane().add(cs);
}
public static void main(String[] args) {
UDP_ME frame = new UDP_ME();
frame.setTitle("梁山伯界面");
frame.setSize(510, 500);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
class Chat_me extends JPanel implements Runnable, ActionListener {
JButton send; //发送按钮
TextArea msg_show;//信息显示框
TextField msg_send; //信息发送框
Thread thread = null; //负责接收数据的线程
/** 初始化界面* */
public Chat_me() {
setLayout(null);
send = new JButton("发送");
msg_show = new TextArea();
msg_send = new TextField();
JLabel jl = new JLabel("信息显示区");
jl.setBounds(0, 0, 500, 20);
add(jl);
/** 添加信息显示框到面板上 */
msg_show.setBounds(0, 20, 500, 300);
msg_show.setEditable(false);
add(msg_show);
JLabel jl1 = new JLabel("信息发送区");
jl1.setBounds(0, 320, 500, 20);
add(jl1);
/** 添加信息发送框到面板上 */
msg_send.setBounds(0, 340, 400, 80);
add(msg_send);
msg_send.addActionListener(this);//为信息发送框设置监听器
/** 添加发送按钮到面板上 */
send.setBounds(400, 360, 100, 40);
add(send);
send.addActionListener(this);//为发送按钮设置监听器
/** 建立并启动线程,负责接收数据 */
thread = new Thread(this); thread.start();
}
/** 实现监听事件 */
public void actionPerformed(ActionEvent e) {
if (e.getSource() == msg_send || e.getSource() == send)
{//外层if语句开始
if (msg_send.getText() != "")
{//内层if开始
//将要发送的数据字符串转换为字节数组
byte buffer[] = msg_send.getText().trim().getBytes();
try {
//获取本机IP地址对象
InetAddress address = InetAddress.getByName("localhost");
//发送数据的数据包,其目标端口是3441,接收方需在这个端口接收
DatagramPacket data_pack = new DatagramPacket(buffer,buffer.length, address, 3441);
//创建发送数据报的套接字
DatagramSocket mail_data = new DatagramSocket();
/** ParagramPacket对象方法调用 */
msg_show.append("数据报目标主机地址:" + data_pack.getAddress()+ "\n");
msg_show.append("数据报目标端口是:" + data_pack.getPort() +"\n");
msg_show.append("数据报长度:" + data_pack.getLength() +"\n");
msg_show.append("梁山伯说: " + msg_send.getText().trim() + "\n");
msg_send.getText();
mail_data.send(data_pack);//发送数据报
}
catch (Exception ex) { }
}//内层if结束
}//外层if结束
}//方法定义结束
/** 线程调用方法,负责数据接收 */
public void run() {
DatagramSocket mail_data = null; //接收数据包的套接字
byte data[] = new byte[8192];//存放接收数据的字节数组
DatagramPacket pack = null; //接收数据的数据报对象
try {
pack = new DatagramPacket(data, data.length);
//使用端口 3445 来接收数据包(因为对方发来的数据报的目标端口是3445)
mail_data = new DatagramSocket(3445);
}
catch (Exception e) { }
/** 利用循环不断接收数据 */
while (true)
{
if (mail_data == null) break;
else
try {
mail_data.receive(pack); //接收数据包
/** 处理接收到的数据,
* 获取收到的数据的实际长度,
* 获取收到的数据包的始发地址,
* 获取收到的数据包的始发端口。
*/
int length = pack.getLength();
InetAddress adress = pack.getAddress();
int port = pack.getPort();
String message = new String(pack.getData(), 0, length);
msg_show.append("收到数据长度 " + length + "\n");
msg_show.append("收到数据来自 " + adress + " 端口 " + port+ "\n");
//将接收到的数据显示在信息显示框中
msg_show.append("祝英台说: " + message + "\n");
}
catch (Exception e) { }
}
}
}
/**UDP_YOU端代码:UDP_YOU.java**/
package udp; // 类包
import java.net.*; import java.io.*; import java.awt.*;
import java.awt.event.*; import javax.swing.*; import javax.swing.event.*;
public class UDP_YOU extends JFrame {
public UDP_YOU() {
Chat_you cs = new Chat_you();
getContentPane().add(cs);
}
public static void main(String[] args) {
UDP_YOU frame = new UDP_YOU();
frame.setTitle("祝英台界面");
frame.setSize(510, 500);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
class Chat_you extends JPanel implements Runnable, ActionListener {
JButton send; //发送按钮
TextArea msg_show;//信息显示框
TextField msg_send; //信息发送框
Thread thread = null; //负责接收数据的线程
/** 下面初始化界面 */
public Chat_you() {
setLayout(null);
send = new JButton("发送");
msg_show = new TextArea();
msg_send = new TextField();
JLabel jl = new JLabel("信息显示区");
jl.setBounds(0, 0, 500, 20);
add(jl);
/** 添加信息显示框到面板上 */
msg_show.setBounds(0, 20, 500, 300);
msg_show.setEditable(false);
add(msg_show);
JLabel jl1 = new JLabel("信息发送区");
jl1.setBounds(0, 320, 500, 20);
add(jl1);
/** 添加信息发送框到面板上 */
msg_send.setBounds(0, 340, 400, 80);
add(msg_send);
msg_send.addActionListener(this);//为信息发送框设置监听器
/** 添加发送按钮到面板上 */
send.setBounds(400, 360, 100, 40);
add(send);
send.addActionListener(this);//为发送按钮设置监听器
/** 建立并启动线程,负责接收数据 */
thread = new Thread(this); thread.start();
}
/** 实现监听事件 */
public void actionPerformed(ActionEvent e) {
if (e.getSource() == msg_send || e.getSource() == send) {
if (msg_send.getText() != "") {
//将要发送的数据字符串转换为字节数组
byte buffer[] = msg_send.getText().trim().getBytes();
try {
//获取本机IP地址对象
InetAddress address = InetAddress.getByName("localhost");
//发送数据的数据包,其目标端口是3445,接收方需在这个端口接收
DatagramPacket data_pack = new DatagramPacket(buffer,
buffer.length, address, 3445);
//创建发送数据报的套接字
DatagramSocket mail_data = new DatagramSocket();
/** ParagramPacket对象方法调用 */
msg_show.append("数据报目标主机地址:" + data_pack.getAddress()+ "\n");
msg_show.append("数据报目标端口是:" + data_pack.getPort() + "\n");
msg_show.append("数据报长度:" + data_pack.getLength() + "\n");
msg_show.append("祝英台说: " + msg_send.getText().trim() + "\n");
msg_send.setText(null);
mail_data.send(data_pack);//发送数据报
}
catch (Exception ex) { }
}
}
}
/** 线程调用方法,负责数据接收 */
public void run() {
DatagramSocket mail_data = null; //接收数据包的套接字
byte data[] = new byte[8192];//存放接收数据的字节数组
DatagramPacket pack = null; //接收数据的数据报对象
try {
pack = new DatagramPacket(data, data.length);
//使用端口 3441 来接收数据包(因为对方发来的数据报的目标端口是3441)
mail_data = new DatagramSocket(3441);
}
catch (Exception e) { }
/** 利用循环不断接收数据 */
while (true) {
if (mail_data == null) break;
else
try {
mail_data.receive(pack); //接收数据包
/** 处理接收到的信息,
*获取收到的数据的实际长度,
*获取收到的数据包的始发地址,
*获取收到的数据包的始发端口,
*将数据转换为字符串。
*/
int length = pack.getLength();
InetAddress adress = pack.getAddress();
int port = pack.getPort();
String message = new String(pack.getData(), 0, length);
msg_show.append("收到数据长度 " + length + "\n");
msg_show.append("收到数据来自" + adress + "端口" +port+ "\n");
//将接收到的数据显示在信息显示框中
msg_show.append("梁山伯说: " + message + "\n");
}
catch (Exception e) { msg_show.append("对方已断开连接"); }
}
}
}
将上面两个程序放在UDP目录下编译,然后,执行服务器和客户程序。
14.5 广播数据报
14.5.1 广播数据报概要
广播数据报类似于电台广播。进行广播的电台需在指定的波段和频率上广播信息,接收者只有将收音机调到指定的波段、频率上才能收听到广播的内容。
广播数据报涉及到地址和端口。Internet的地址是以a.b.c.d的格式给出,该地址的一部分代表用户自己的主机,而另一部分代表用户所在的网络。当a小于128,那么b.c.d就用来表示主机,这类地址称为A类地址;如果a大于等于128并且小于192,则a.b表示网络地址,而c.d表示主机地址,这类地址称为B类地址;如果a大于或等于192,小于224则网络地址是a.b.c。而d表示主机地址,这类地址称为C类地址;224.0.0.0与224.255.255.255是保留地址,称为D类地址。
广播或接收广播的主机都必须加入到同一个D类地址。一个D类地址也称为一个广播组。加入到同一个广播组的主机可以在某个端口上广播信息,也可以在某个端口号上接收信息。
14.5.2 MultiCastSocket类
多播数据报套接字用于发送和接收IP多播数据包。MulticastSocket 是一种(UDP)DatagramSocket。它具有加入Internet上其他多播主机所属“组”的功能(多播组通过D类IP地址和标准UDP端口号指定)。D类IP地址的范围是224.0.0.0~239.255.255.255(包括两者)。地址224.0.0.0被保留(不许使用)。首先使用所需端口创建 MulticastSocket对象,然后调用 joinGroup(InetAddress groupAddr)方法来加入多播组。
1.MulticastSocket的构造方法
(1)MulticastSocket():创建多播套接字。
(2)MulticastSocket(int port):创建多播套接字,并将其绑定到特定端口。
(3) MulticastSocket(SocketAddress bindaddr):创建绑定到指定套接字地址的MulticastSocket对象。
这3个构造方法都会抛出IOException异常或SecurityException异常,所以必须在try-catch 结构中创建MulticastSocket对象。
2.MulticastSocket的实用方法
(1) public void setTimeToLive(int ttl) throws IOException:设置在对应MulticastSocket上发出的多播数据报的默认生存时间,以便控制多播的范围。ttl必须在0≤ttl≤255范围内,否则将抛出 IllegalArgumentException。tt1是指多播数据报的默认生存时间。
(2) public int getTimeToLive() throws IOException:获取在套接字上发出的多播数据报的默认生存时间。
(3) public void joinGroup(InetAddress mcastaddr) throws IOException:加入多播组,mcastaddr为要加入的多播地址。
(3) public void leaveGroup(InetAddress mcastaddr) throws IOException:离开多播组,mcastaddr为要离开的多播地址。
14.5.3 MulticastSocket的应用
【例14.8】利用MulticastSocket实现数据报的广播。
【程序说明】本例中,在广播端输入要广播的信息,单击“开始发送”按钮或按Enter键触发动作事件,将数据广播出去,单击“停止发送”按钮则终止本次数据广播;在接收端用户通过单击“开始接收”按钮实现数据接收,单击“停止接收”按钮则终止本次数据接收。
程序名:Receiver.java && BroadCast.java。
【程序源代码】
/** 接收端:Receiver.Java */
import java.net.*; import java.awt.*; import java.awt.event.*;
public class Receiver extends Frame implements Runnable, ActionListener {
int port; //多播的端口
InetAddress group = null; //多播组的地址
MulticastSocket socket = null; //多点广播套接字
Button reveive_start, reveive_stop; //开始接收按钮和停止接收按钮
TextArea msg_on_receive, //显示正在接收的信息
msg_received; //显示己经接收的信息
Thread thread; //负责接收信息的线程
boolean if_stop = false; //线程状态信号量
public Receiver() {
/** 下面初始化界面 */
super("信息接收端");
thread = new Thread(this);
reveive_start = new Button("开始接收");
reveive_stop = new Button("停止接收");
reveive_stop.addActionListener(this);
reveive_start.addActionListener(this);
msg_on_receive = new TextArea(10, 10);
msg_on_receive.setForeground(Color.blue);
msg_received = new TextArea(10, 10);
Panel north = new Panel();
north.add(reveive_start);
north.add(reveive_stop);
add(north, BorderLayout.NORTH);
Panel center = new Panel();
center.setLayout(new GridLayout(1, 2));
center.add(msg_on_receive);
center.add(msg_received);
add(center, BorderLayout.CENTER); validate();
/** 下面建立多点广播套接字,并将该套接字加入广播组中 */
port = 5858; //设置多播组的监听端口
try {
//设置广播组的地址为239.255.8.0.
group = InetAddress.getByName("239.255.8.0");
//多点广播套接字将在 port 端口广播
socket = new MulticastSocket(port);
/**加入广播组。加入 group 后,socket 发送的数据报
*可以被加入到 group 中的成员接收到
*/
socket.joinGroup(group);
}
catch (Exception e) { }
setBounds(100, 50, 360, 380);
setVisible(true);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
/** 动作事件实现方法,负责开始和停止接收数据 */
public void actionPerformed(ActionEvent e) {
if (e.getSource() == reveive_start) //响应开始按钮事件
{
reveive_start.setBackground(Color.blue);
reveive_stop.setBackground(Color.gray);
/** 创建线程,实现数据接收 */
if (!(thread.isAlive())) { thread = new Thread(this); }
try {
thread.start();
if_stop = false;
}
catch (Exception ee) { }
}
if (e.getSource() == reveive_stop) //响应停止按钮事件
{
reveive_start.setBackground(Color.gray);
reveive_stop.setBackground(Color.blue);
thread.interrupt(); //中断线程,停止数据接收
if_stop = true;
}
}
/** 线程调用方法,负责数据接收和显示 */
public void run() {
while (true) //利用循环接收定时广播传来的数据
{ byte data[] = new byte[8192];
DatagramPacket packet = null;
//待接收的数据包
packet = new DatagramPacket(data, data.length, group, port);
try {
socket.receive(packet); //接收广播数据并将其放在指定数据包中
String message = new String(packet.getData(), 0, packet .getLength());
msg_on_receive.setText("正在接收的内容:\n" + message);
msg_received.append(message + "\n");
}
catch (Exception e) { }
if (if_stop == true) { break; }//若线程中断,则终止线程
}
}
public static void main(String args[]) {
new Receiver();
}
}
/** 广播端:BroadCast.Java */
import java.net.*; import java.awt.*; import java.awt.event.*;
public class BroadCast extends Frame implements Runnable, ActionListener {
int port = 5858; //多播的端口
InetAddress group = null; //多播组的地址
MulticastSocket socket = null;//多点广播套接字
Button send, stop; //开始发送按钮和停止发送按钮
TextArea msg_show; //信息显示框
TextField msg_send; //信息发送框
Thread thread; //负责发送信息的线程
boolean if_stop = false; //线程状态信号量
public BroadCast()
{ /** 初始化界面 */
super("广播信息台");
send=new Button("开始发送");
stop=new Button("停止发送");
send.addActionListener(this);
stop.addActionListener(this);
Panel south=new Panel();
south.add(send); south.add(stop);
Label tip1=new Label("显示正在广播的信息");
msg_show=new TextArea();
msg_show.setEditable(false);
Panel north=new Panel();
north.setLayout(new BorderLayout());
north.add(tip1,BorderLayout.NORTH); north.add(msg_show,BorderLayout.CENTER);
Label tip2=new Label("输入要广播的信息");
msg_send=new TextField();
msg_send.addActionListener(this);
Panel center=new Panel();
center.setLayout(new BorderLayout());
center.add(tip2,BorderLayout.NORTH); center.add(msg_send,BorderLayout.CENTER);
setLayout(new BorderLayout());
add(south,BorderLayout.SOUTH); add(north,BorderLayout.NORTH);
add(center,BorderLayout.CENTER); validate();
/** 建立多点广播套接字,并将该套接字加入广播组中以进行数据广播 */
try
{
//设置广播组的地址为239.255.8.0
group=InetAddress.getByName("239.255.8.0");
//多点广播套接字将在port端口广播
socket=new MulticastSocket(port);
//多点广播套接字发送数据报范围为本地网络,数据默认生存时间为1
socket.setTimeToLive(1);
socket.joinGroup(group); //加入广播组
}
catch(Exception e) { msg_show.append("Error: "+ e); }
setBounds(100,50,360,380);
setVisible(true);
addWindowListener(new WindowAdapter() //为窗口关闭事件添加监听器
{
public void windowClosing(WindowEvent e)
{ System.exit(0); }
});
}
/** 动作事件实现方法,负责开始和停止广播数据 */
public void actionPerformed(ActionEvent e) {
//响应广播数据事件
if (e.getSource() == send || e.getSource() == msg_send)
{
send.setBackground(Color.blue);
stop.setBackground(Color.gray);
/** 创建线程,实现数据广播* */
try {
thread = new Thread(this);
thread.start();
if_stop = false;
}
catch (Exception ee) { }
}
//响应停止广播数据事件
if (e.getSource() == stop)
{
send.setBackground(Color.gray);
stop.setBackground(Color.blue);
msg_show.setText(null);
msg_send.setText(null);
thread.stop();
if_stop = true;
}
}
/** 线程调用方法,负责数据广播和显示 */
public void run() {
while (true) //利用循环定时广播数据
{
try {
DatagramPacket packet = null; //待广播的数据包
byte data[] = msg_send.getText().getBytes();
packet = new DatagramPacket(data, data.length, group, port);
socket.send(packet); //广播数据包
msg_show.append("正在发送的信息: \n" + new String(data) + "\n");
Thread.sleep(1000); //每隔一秒广播一次
}
catch (Exception e) { msg_show.append("Error: " + e); }
}
}
public static void main(String args[]) {
new BroadCast();
}
}
【程序运行结果】
程序运行结果如图14-6所示。
图14-6 例14.8程序运行结果
14.6 本 章 小 结
本章开始便以URL为主线,讲解了如何通过URL类访问WWW网络资源。由于使用URL十分方便、直观,尽管功能不是很强,还是值得推荐的一种网络编程方法,尤其是对于初学者更易于接受。从本质上讲,URL网络编程在传输层使用的还是TCP协议。
接下来几节是以Socket接口和C/S网络编程模型为主线,主要讲解了如何用Java实现基于TCP的C/S结构,主要用到的类有Socket、ServerSocket;如何用Java实现基于UDP的C/S结构;此外还讨论了一种特殊的数据传输方式——广播数据报,这种方式是UDP所特有的,主要用到的类有DatagramSocket、DatagramPacket以及MulticastSocket。
14.7 习 题
1.怎样创建服务器套接字?可以使用什么端口号?如果请求的端口号已在使用,会发生什么现象?一个端口能与多少个客户机连接?
2.服务器套接字与客户机套接字之间有什么区别?
3.客户端程序如何开始一个连接?
4.服务器怎样接收连接请求?
5.数据如何在客户机和服务器之间传输?可以传输对象吗?
6.怎样让服务器为多个客户机服务?
7.能否编写一个程序,从远程主机上获取文件?能否更新远程主机上的文件?
8.编写一个聊天程序。
9. 编写一个applet,显示对一个web页面的访问次数。次数存储在服务器端的文件中。