Skip to content

Latest commit

 

History

History
executable file
·
653 lines (470 loc) · 35 KB

File metadata and controls

executable file
·
653 lines (470 loc) · 35 KB
title 网络编程和通信

[TOC]

网络编程和通信

网络

TCP/IP协议

TCP/IP协议(传输控制协议)由网络层的IP协议和传输层的TCP协议组成。IP层负责网络主机的定位,数据传输的路由,由IP地址可以唯一的确定Internet上的一台主机。TCP层负责面向应用的可靠的或非可靠的数据传输机制,这是网络编程的主要对象。

端口

Internet上传输的数据都带有标识目的主机与端口号的地址信息,主机的地址由$32$位的IP地址标识,IP协议通过该地址把数据发送到正确的目的主机; 端口号由一个$16$位的数字标识,TCP与UDP协议根据端口号把数据传送给正确的应用程序。

端口号的范围是$0~65535$,其中$1~1023$之间的端口号是为HTTP、FTP等系统应用保留的,FTP协议的端口号是$21$,HTTP协议的端口号是$80$,Telnet协议的端口号是$23$,用户应用程序只能使用$1024$以上的端口号,其中$1024~4999$可任意被用户用作客户端套接字端口,$5000~65535$可任意被用户用作服务端套接字端口。

Java提供的网络功能

针对网络通信的不同层次,Java提供的网络功能有四大类:InetAddress 、URLs、Sockets、Datagram。

  • InetAddress: 面向IP层,用于标识网络上的硬件资源。

  • URL和URLConnection: 面向应用层,通过URL,Java程序可以直接送出或读入网络上的数据。

  • Sockets

    • 面向传输层。
    • Datagram则使用UDP协议,是另一种网络传输方式,它把数据的目的地纪录在数据包中,然后直接放在网络上。
  • Datagram:

    • 面向传输层。
    • Sockets使用的是TCP协议,这是传统网络程序最常用的方式,可以想象为两个不同的程序通过网络的通信信道进行通信。

java.net包中的主要的类

  • 面向IP层的类:InetAddress

  • 面向应用层的类:URL、URLConnection

  • 面向传输层的类:

    • TCP协议相关类:Socket、ServerSocket
    • UDP协议相关类:DatagramPacket、DatagramSocket、 MulticastSocket

可能产生的异常:

  • BindException、
  • ConnectException、
  • MalformedURLException、
  • NoRouteToHostException、
  • ProtocolException、
  • SocketException、
  • UnknownHostException、
  • UnknownServiceException

Socket通信

Socket

Socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄。网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket,一个Socket由一个IP地址和一个端口号唯一确定。应用程序通常通过"套接字"向网络发出请求或者应答网络请求。 Socket是TCP/IP协议的一个十分流行的编程界面,但是,Socket所支持的协议种类也不光TCP/IP一种,因此两者之间是没有必然联系的。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。

Socket通讯过程: 服务端监听某个端口是否有连接请求,客户端向服务端发送连接请求,服务端收到连接请求向客户端发出接收消息,这样一个连接就建立起来了。客户端和服务端都可以相互发送消息与对方进行通讯。

Socket的基本工作过程包含以下四个步骤:

  1. 创建Socket;
  2. 打开连接到Socket的输入输出流;
  3. 按照一定的协议对Socket进行读写操作;
  4. 关闭Socket。

Socket通信机制

提供了两种通信方式:

  • 有连接方式(TCP) 有连接方式中,通信双方在开始时必须进行一次连接过程,建立一条通信链路。通信链路提供了可靠的、全双工的字节流服务。

通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。

  • 无连接方式(UDP数据报)。 无连接方式中,通信双方不存在一个连接过程,一次网络I/O以一个数据报形式进行,而且每次网络I/O可以和不同主机的不同进程进行。无连接方式开销小于有连接方式,但是所提供的数据传输服务不可靠,不能保证数据报一定到达目的地。

每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。

Java语言同时支持有连接和数据报通信方式,在这两种方式中都采用了Socket表示通信过程中的端点。

TCP与UDP区别:

TCP特点:

1、TCP是面向连接的协议,通过三次握手建立连接,通讯完成时要拆除连接,由于TCP是面向连接协议,所以只能用于点对点的通讯。而且建立连接也需要消耗时间和开销。

2、TCP传输数据无大小限制,进行大数据传输。

3、TCP是一个可靠的协议,它能保证接收方能够完整正确地接收到发送方发送的全部数据。

UDP特点:

  1、UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。

  2、UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内。

  3、UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。

TCP与UDP应用:

1、TCP在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输。但是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此TCP传输的效率不如UDP高。

2,UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。例如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。

Socket通信

  • 端到端的连接与通信
  • 网络上的两个程序(进程)通过一个双向的通信连接实现数据的交换。
  • 双向链路的一端称为一个socket(套接字)
  • 主机—端口(用于区分同一台主机上的不同的通信应用进程:01023系统 102465535用户)

Socket通信过程

服务器端的程序首先选择一个端口(port)注册,然后调用accept()方法对此端口进行监听,即等待其它程序的连接申请。如果客户端的程序申请和此端口连接,那么服务器端就利用accept()方法来取得这个连接的Socket。

客户端的程序建立Socket时必须指定服务器的地址(host)和通信的端口(port),这个端口必须与服务器端监听的端口保持一致。

URL通信

URL(Uniform Resource Locator):统一资源定位器,它表示Internet/Intranet上一个资源的引用或地址,这些资源可以是一个文件、一个目录或一个对象。

URL是由一个字符串来描述的,URL包括协议和资源名称两部分,协议表示访问资源所需的协议,如HTTP、FTP等;资源名称表示要访问的资源地址。

InetAddress类

InetAddress类是IP地址的封装类,描述了32位或128位IP地址。InetAddress类含有IP地址和域名,借助于这个类,既可以通过主机域名获得IP地址,也可通过主机IP地址获得域名。

InetAddress类没有构造函数,因此不能用new来构造一个InetAddress实例。通常是用它提供的静态方法来获取: public static InetAddress getByName(String host) :host可以是一个机器名,也可以是一个形如“%d.%d.%d.%d”的IP地址或一个DSN域名。 public static InetAddress getLocalHost() public static InetAddress[] getAllByName(String host) 这三个方法会产生UnknownHostException异常,应在程序中捕获处理。

InetAddress类的几个主要方法: String getHostName():获取InetAddress对象所含的域名 getHostAddress():获取InetAddress对象所含的IP地址

URL类

URL类的构造方法

(1) public URL(String spec) (2) public URL(String protocol, String host, String file) (3) public URL(String protocol,String host,String port,String file) (4) public URL(URL context,String spec) 注意:类URL的构造方法都声明抛出非运行时异常(MalformedURLException),因此生成URL对象时,我们必须要对这一异常进行处理,用try-catch语句进行捕获。

URL类的成员方法

成员方法 功能说明
public int getPort() 获取端口号。若端口号未设置,返回–1
public String getProtocol() 获取协议名。若协议未设置,返回null
public String getHost() 获取主机名。若主机名未设置,返回null
public String getFile() 获取文件名。若文件名未设置,返回null
public boolean equals(Object obj) 与指定的URL对象obj进行比较,如果相同返回true,否则返回false
public final InputStream openStream() 获取输入流。若获取失败,则抛出一个java.io.Exception异常。
public URLConnection openConnection() 生成关联该URL的一个URLConnection对象
String toString() 将此URL对象转换成字符串形式

URLConnection类

通过URLConnection类,可以在应用程序和URL资源之间进行交互,既可以从URL中读取数据,也可以向URL中发送数据。URLConnection类表示了应用程序和URL资源之间的通信连接。

1.创建URLConnection类的对象 URLConnection类是一个抽象类,创建URLConnection对象分两步完成:创建一个URL对象,然后调用该URL对象的openConnection()方法返回一个对应其URL地址的URLConnection对象。

2.向服务器端写数据 首先建立输出数据流: PrintStream out=new PrintStream(con.getOutputStream()); 然后向服务器写入信息: out.println(String data);

3.从服务器端读数据 建立输入数据流: InputStreamReader ins=new InputStreamReader(con.getInputStream()); BufferedReader in=new BufferedReader(ins); 或: DataInputStream dis=new DataInputStream(con.getInputStream()); 然后从服务器读信息: in.readLine(); 或:is.readLine();

TCP Socket通信

TCP(Transport Control Protocol) 两主机之间有连接的、可靠的、端对端(end-to-end)的数据流的传输 如http, ftp, telnet 的传输层均使用此协议

为了支持TCP/IP面向连接的网络程序的开发,java.net包提供了Socket类与ServerSocket类,ServerSocket类和Socket类均直接继承于Java的Object根类。 其中,ServerSocket类用于服务端程序,它有一个accept方法专门用来监听客户端的连接,并产生一个与客户端连接相对应的Socket对象; Socket类则是服务端程序和客户端程序都要用到的类,该类专门用来处理连接双方的数据通信。 一个Socket由一个IP地址和一个端口号唯一确定。

URL通信与Socket通信的区别

URL通信与Socket通信都是面向连接的通信。区别在于:

  • (1)Socket通信方式为主动等待客户端的服务请求方式,而URL通信方式为被动等待客户端的服务请求方式。

  • (2)利用Socket进行通信时,在服务器端运行了一个Socket通信程序,不停地监听客户端的连接请求,当接到客户端请求后,马上建立连接并进行通信。利用URL进行通信时,在服务器端常驻一个CGI程序,但它一直处于睡眠状态,只有当客户端的连接请求到达时才被唤醒,然后建立连接并进行通信。

  • (3)在Socket通信方式中,服务器端的程序可以打开多个线程与多个客户端进行通信,并且还可以通过服务器使各个客户端之间进行通信,这种方式适合于一些较复杂的通信。而在URL通信方式中,服务器端的程序只能与一个客户进行通信,这种方式比较适合于B/S通信模式。

使用Socket通信过程

利用socket进行有连接的通信过程包括以下三个步骤:

  • (1) 创建socket,建立连接 java.net包中的两个类Socket与ServerSocket分别表示连接的Client端和Server端,进行网络通信的方法也都封装在这两个类中。建立连接首先要创建这两个类的对象并把它们关联起来。

  • (2) 打开连接到Socket的输入输出流,按照一定的协议对Socket进行读写操作 连接建立后,要进一步获取连接上的I/O流,并通过这些流进行数据传输。

  • (3) 关闭Socket

Socket类

构造方法:
构造方法 功能说明
public Socket(InetAddress address,int port) 使用InetAddress对象所表示的IP地址以及端口号(port)向服务器发起连接,若成功则创建Socket对象,否则抛出异常
public Socket(InetAddress address,int port, boolean stream) 同上,若布尔参数为true,则采用流式通信方式,否则为数据报通信方式
public Socket(String host,int port) 使用指定端口port和主机host,向服务器发起连接,若成功则创建Socket对象,否则抛出异常
public Socket(String host,int port,Boolean stream) 同上,布尔参数为true,则采用流式通信方式
常用成员方法
常用成员方法 功能说明
public void close() 关闭Socket,释放资源
public InputStream getInputStream() 获取与Socket相关联的字节输入流,用于从Socket中读数据
public OutputStream getOutputStream() 获取与Socket相关联的字节输出流,用于向Socket中写数据
public int getLocalPort() 返回本地Socket中的端口号
public int getPort() 返回对方Socket中的端口号
public InetAddress getLocalAddress() 返回本地Socket中IP的InetAddress对象
public InetAddress getInetAddress() 返回对方Socket中IP的InetAddress对象
public void setSoTimeout(int timeout) 设置客户端的连接超时时间。当程序调用Socket输入流对象的read方法读取数据后,程序将处于阻塞状态,直到setSoTimeout方法所设置的timeout时间超时为止。如果timeout为0,将表示read方法无限期地等待数据来读取

ServerSocket类

构造方法:
构造方法 功能说明
public ServerSocket(int port) throws IOException 在指定的端口创建一个服务器Socket对象,默认可接收50个客户端连接。如果端口号设置为0,则自动挑选一个空闲的端口号。如果打开这个套接字时出现I/O错误,则抛出异常对象IOException
public ServerSocket(int port,int count) throws IOException 在指定的端口port创建一个服务器 Socket对象,并说明服务器端所能支持的最大连接数count,如果超过此数目,客户端连接将被拒绝
public ServerSocket(int port,int count, InetAddress bindArr) throws IOException 在指定的端口port创建一个服务器 Socket对象,并说明服务器端所能支持的最大连接数count,bindAddr可用来绑定服务器程序所使用的IP地址。如果运行服务端程序的机器配置了多个IP地址(多网卡),这个构造函数将非常有用
常用成员方法
常用成员方法 功能说明
public Socket accept() throws IOException 监听客户端的连接请求。accept方法调用后,服务器程序将处于阻塞状态,直到监听到客户端连接,返回一个新创建的Socket对象。接下来利用这个返回的Socket对象与客户端进行数据收发
public void setSoTimeout(int timeout) 设置服务器程序等待客户端连接的最长时间timeout,时间单位为毫秒。如果timeout为0,将表示无限期等待客户端连接。该方法必须在accept方法之前被调用
public void close() throws IOException 关闭套接字资源

数据报通信

用户数据报协议UDP是传输层的无连接通信协议。数据报是一种在网络中独立传播的自身包含地址信息的消息,它能否到达目的地、到达的时间以及到达时内容能否保持不变,这些都是不能保证的。由于UDP通信速度较快,所以常常被应用在某些无须实时交互、准确性要求不高、但传输速度要求较高的场合。 Java.net软件包中的DatagramSocket类和DatagramPacket类为实现UDP通信提供了支持。DatagramSocket用于在程序中间建立传送数据报的通信连接,DatagramPacket则用来表示一个数据报。

数据报方式的通信过程

  • (1) 创建数据报Socket;
  • (2) 构造用于接收或发送的数据报,并调用所创建Socket的receive()方法进行数据报接收或调用send()发送数据报。
  • (3)通信结束,关闭Socket。

DatagramSocket类

DatagramSocket类的三个构造方法如下:

  • (1) DatagramSocket():创建DatagramSocket对象并与本地主机某个可用端口相连。
  • (2) DatagramSocket(int port):创建DatagramSocket对象并与指定端口相连。
  • (3) DatagramSocket(int port,InetAddress iaddr):创建一个于本地地址绑定的DatagramSocket对象。

基于TCP的socket编程

服务器程序编写

  • ①调用ServerSocket(int port)创建一个服务器端套接字,并绑定到指定端口上;
  • ②调用accept(),监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字。
  • ③调用Socket类的getOutputStream()和getInputStream获取输出流和输入流,开始网络数据的发送和接收。
  • ④最后关闭通信套接字。

客户端程序编写:

  • ①调用Socket()创建一个流套接字,并连接到服务器端;
  • ②调用Socket类的getOutputStream()和getInputStream获取输出流和输入流,开始网络数据的发送和接收。
  • ③最后关闭通信套接字。

TCP Socket Demo

下面给出一个客户端服务端 TCP 通信的 Demo,该客户端在 20006 端口请求与服务端建立 TCP 连接,客户端不断接收键盘输入,并将其发送到服务端,服务端在接收到的数据前面加上“echo”字符串,并将组合后的字符串发回给客户端,如此循环,直到客户端接收到键盘输入“bye”为止。

客户端代码如下:

package zyb.org.client;  

import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.InputStreamReader;  
import java.io.PrintStream;  
import java.net.Socket;  
import java.net.SocketTimeoutException;  

public class Client1 {  
    public static void main(String[] args) throws IOException {  
        //客户端请求与本机在20006端口建立TCP连接   
        Socket client = new Socket("127.0.0.1", 20006);  
        client.setSoTimeout(10000);  
        //获取键盘输入   
        BufferedReader input = new BufferedReader(new InputStreamReader(System.in));  
        //获取Socket的输出流,用来发送数据到服务端    
        PrintStream out = new PrintStream(client.getOutputStream());  
        //获取Socket的输入流,用来接收从服务端发送过来的数据    
        BufferedReader buf =  new BufferedReader(new InputStreamReader(client.getInputStream()));  
        boolean flag = true;  
        while(flag){  
            System.out.print("输入信息:");  
            String str = input.readLine();  
            //发送数据到服务端    
            out.println(str);  
            if("bye".equals(str)){  
                flag = false;  
            }else{  
                try{  
                    //从服务器端接收数据有个时间限制(系统自设,也可以自己设置),超过了这个时间,便会抛出该异常  
                    String echo = buf.readLine();  
                    System.out.println(echo);  
                }catch(SocketTimeoutException e){  
                    System.out.println("Time out, No response");  
                }  
            }  
        }  
        input.close();  
        if(client != null){  
            //如果构造函数建立起了连接,则关闭套接字,如果没有建立起连接,自然不用关闭  
            client.close(); //只关闭socket,其关联的输入输出流也会被关闭  
        }  
    }  
}  

服务端需要用到多线程,这里单独写了一个多线程类,代码如下:

package zyb.org.server;  

import java.io.BufferedReader;  
import java.io.InputStreamReader;  
import java.io.PrintStream;  
import java.net.Socket;  

/** 
 * 该类为多线程类,用于服务端 
 */  
public class ServerThread implements Runnable {  

    private Socket client = null;  
    public ServerThread(Socket client){  
        this.client = client;  
    }  

    @Override  
    public void run() {  
        try{  
            //获取Socket的输出流,用来向客户端发送数据  
            PrintStream out = new PrintStream(client.getOutputStream());  
            //获取Socket的输入流,用来接收从客户端发送过来的数据  
            BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));  
            boolean flag =true;  
            while(flag){  
                //接收从客户端发送过来的数据  
                String str =  buf.readLine();  
                if(str == null || "".equals(str)){  
                    flag = false;  
                }else{  
                    if("bye".equals(str)){  
                        flag = false;  
                    }else{  
                        //将接收到的字符串前面加上echo,发送到对应的客户端  
                        out.println("echo:" + str);  
                    }  
                }  
            }  
            out.close();  
            client.close();  
        }catch(Exception e){  
            e.printStackTrace();  
        }  
    }  

}  

服务端处理 TCP 连接请求的代码如下:

package zyb.org.server;  

import java.net.ServerSocket;  
import java.net.Socket;  

public class Server1 {  
    public static void main(String[] args) throws Exception{  
        //服务端在20006端口监听客户端请求的TCP连接  
        ServerSocket server = new ServerSocket(20006);  
        Socket client = null;  
        boolean f = true;  
        while(f){  
            //等待客户端的连接,如果没有获取连接  
            client = server.accept();  
            System.out.println("与客户端连接成功!");  
            //为每个客户端连接开启一个线程  
            new Thread(new ServerThread(client)).start();  
        }  
        server.close();  
    }  
}  

基于UDP的socket编程

接收端程序编写:

①调用DatagramSocket(int port)创建一个数据报套接字,并绑定到指定端口上; ②调用DatagramPacket(byte[] buf, int length),建立一个字节数组以接收UDP包 。 ③调用DatagramSocket类的receive(),接收UDP包。 ④最后关闭数据报套接字。

发送端程序编写:

①调用DatagramSocket()创建一个数据报套接字; ②调用DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port),建立要发送的UDP包。 ③调用DatagramSocket类的send(),发送UDP包。 ④最后关闭数据报套接字。

DatagramSocket类的常用方法如下:

(1) void receive(DatagramPacket packet)throws IOException receive()方法将使程序中的线程一直处于阻塞状态,直到从当前socket中接收到信息时,将收到的信息存储在receive()方法的对象参数packet的存储机构中。 (2) void send(DatagramPacket packet)throws IOException send()方法将其参数DatagramPacket对象packet中包含的数据报文发送到所指定的IP地址主机的指定端口。 (3) void setSotimeout(int timeout) throws IOException 当程序调用DatagramSocket的receive方法以读取数据后,程序将处于阻塞状态,知道setSoTimeout方法所设置时间超时为止。 (4) void close() 关闭数据报套接字,它不会抛出异常对象。

DatagramPacket类

当DatagramSocket套接字创建后,就可以用它来发送和接收数据报了。数据报是一个DatagramPacket类的对象,创建时必须指明目的端的IP地址和端口号,这样发送到网络上的数据报才可以被网关路由。DatagramPacket类的构造方法,分别对应接收数据报和发送数据报:

(1) public DatagramPacket(byte[] buf,int length) 创建接收length长度的数据报对象。其中buf为接收数据报的字节数组,length为读取的字节数,length必须小于等于buf.length。

(2) public DatagramPacket(byte[]buf, int offset, int length, InetAddress address, int port) 这个构造方法用来创建发送数据报对象。其中,buf代表发送数据报的字节数组;length代表发送数据报的长度;address代表发送数据报的目的地址,即接收者的IP地址;port代表发送数据报的端口号。

DatagramPacket类的常用方法如下:

(1) public InetAddress getAddress() 返回发出数据报或接收数据报的机器的IP地址。 (2) public int getPort() 返回发出数据报或接收数据报的远程主机的端口号。 (3) public void setAddress(InetAddress iaddr) 设置接收数据报的机器的IP地址。

UDP Socket Demo

这里有一点需要注意:UDP 程序在 receive()方法处阻塞,直到收到一个数据报文或等待超时。由于 UDP 协议是不可靠协议,如果数据报在传输过程中发生丢失,那么程序将会一直阻塞在 receive()方法处,这样客户端将永远都接收不到服务器端发送回来的数据,但是又没有任何提示。为了避免这个问题,我们在客户端使用 DatagramSocket 类的 setSoTimeout()方法来制定 receive()方法的最长阻塞时间,并指定重发数据报的次数,如果每次阻塞都超时,并且重发次数达到了设置的上限,则关闭客户端。

下面给出一个客户端服务端 UDP 通信的 Demo(没有用多线程),该客户端在本地 9000 端口监听接收到的数据,并将字符串"Hello UDPserver"发送到本地服务器的 3000 端口,服务端在本地 3000 端口监听接收到的数据,如果接收到数据,则返回字符串"Hello UDPclient"到该客户端的 9000 端口。在客户端,由于程序可能会一直阻塞在 receive()方法处,因此这里我们在客户端用 DatagramSocket 实例的 setSoTimeout()方法来指定 receive()的最长阻塞时间,并设置重发数据的次数,如果最终依然没有接收到从服务端发送回来的数据,我们就关闭客户端。

客户端代码如下:

package zyb.org.UDP;  

import java.io.IOException;  
import java.io.InterruptedIOException;  
import java.net.DatagramPacket;  
import java.net.DatagramSocket;  
import java.net.InetAddress;  

public class UDPClient {  
    private static final int TIMEOUT = 5000;  //设置接收数据的超时时间  
    private static final int MAXNUM = 5;      //设置重发数据的最多次数  
    public static void main(String args[])throws IOException{  
        String str_send = "Hello UDPserver";  
        byte[] buf = new byte[1024];  
        //客户端在9000端口监听接收到的数据  
        DatagramSocket ds = new DatagramSocket(9000);  
        InetAddress loc = InetAddress.getLocalHost();  
        //定义用来发送数据的DatagramPacket实例  
        DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),loc,3000);  
        //定义用来接收数据的DatagramPacket实例  
        DatagramPacket dp_receive = new DatagramPacket(buf, 1024);  
        //数据发向本地3000端口  
        ds.setSoTimeout(TIMEOUT);              //设置接收数据时阻塞的最长时间  
        int tries = 0;                         //重发数据的次数  
        boolean receivedResponse = false;     //是否接收到数据的标志位  
        //直到接收到数据,或者重发次数达到预定值,则退出循环  
        while(!receivedResponse && tries<MAXNUM){  
            //发送数据  
            ds.send(dp_send);  
            try{  
                //接收从服务端发送回来的数据  
                ds.receive(dp_receive);  
                //如果接收到的数据不是来自目标地址,则抛出异常  
                if(!dp_receive.getAddress().equals(loc)){  
                    throw new IOException("Received packet from an umknown source");  
                }  
                //如果接收到数据。则将receivedResponse标志位改为true,从而退出循环  
                receivedResponse = true;  
            }catch(InterruptedIOException e){  
                //如果接收数据时阻塞超时,重发并减少一次重发的次数  
                tries += 1;  
                System.out.println("Time out," + (MAXNUM - tries) + " more tries..." );  
            }  
        }  
        if(receivedResponse){  
            //如果收到数据,则打印出来  
            System.out.println("client received data from server:");  
            String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) +   
                    " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort();  
            System.out.println(str_receive);  
            //由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数,  
            //所以这里要将dp_receive的内部消息长度重新置为1024  
            dp_receive.setLength(1024);     
        }else{  
            //如果重发MAXNUM次数据后,仍未获得服务器发送回来的数据,则打印如下信息  
            System.out.println("No response -- give up.");  
        }  
        ds.close();  
    }    
}   

服务端代码如下:

package zyb.org.UDP;  

import java.io.IOException;  
import java.net.DatagramPacket;  
import java.net.DatagramSocket;  

public class UDPServer {   
    public static void main(String[] args)throws IOException{  
        String str_send = "Hello UDPclient";  
        byte[] buf = new byte[1024];  
        //服务端在3000端口监听接收到的数据  
        DatagramSocket ds = new DatagramSocket(3000);  
        //接收从客户端发送过来的数据  
        DatagramPacket dp_receive = new DatagramPacket(buf, 1024);  
        System.out.println("server is on,waiting for client to send data......");  
        boolean f = true;  
        while(f){  
            //服务器端接收来自客户端的数据  
            ds.receive(dp_receive);  
            System.out.println("server received data from client:");  
            String str_receive = new String(dp_receive.getData(),0,dp_receive.getLength()) +   
                    " from " + dp_receive.getAddress().getHostAddress() + ":" + dp_receive.getPort();  
            System.out.println(str_receive);  
            //数据发动到客户端的3000端口  
            DatagramPacket dp_send= new DatagramPacket(str_send.getBytes(),str_send.length(),dp_receive.getAddress(),9000);  
            ds.send(dp_send);  
            //由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数,  
            //所以这里要将dp_receive的内部消息长度重新置为1024  
            dp_receive.setLength(1024);  
        }  
        ds.close();  
    }  
}  

需要注意的地方

#####UDP 套接字和 TCP 套接字的一个微小但重要的差别:UDP 协议保留了消息的边界信息。

DatagramSocket 的每一次 receive()调用最多只能接收调用一次 send()方法所发送的数据,而且,不同的 receive()方法调用绝对不会返回同一个 send()方法所发送的额数据。

当在 TCP 套接字的输出流上调用 write()方法返回后,所有调用者都知道数据已经被复制到一个传输缓存区中,实际上此时数据可能已经被发送,也有可能还没有被传送,而 UDP 协议没有提供从网络错误中恢复的机制,因此,并不对可能需要重传的数据进行缓存。这就意味着,当send()方法调用返回时,消息已经被发送到了底层的传输信道中。

#####UDP 数据报文所能负载的最多数据,亦及一次传送的最大数据为 65507 个字节。

当消息从网络中到达后,其所包含的数据被 TCP 的 read()方法或 UDP 的 receive()方法返回前,数据存储在一个先进先出的接收数据队列中。对于已经建立连接的 TCP 套接字来说,所有已接受但还未传送的字节都看作是一个连续的字节序列。然而,对于 UDP 套接字来说,接收到的数据可能来自不同的发送者,一个 UDP 套接字所接受的数据存放在一个消息队列中,每个消息都关联了其源地址信息,每次 receive()调用只返回一条消息。如果 receive()方法在一个缓存区大小为 n 的 DatagramPacket 实例中调用,而接受队里中的第一条消息的长度大于 n,则 receive()方法只返回这条消息的前 n 个字节,超出部分会被自动放弃,而且对接收程序没有任何消息丢失的提示!

出于这个原因,接受者应该提供一个有足够大的缓存空间的 DatagramPacket 实例,以完整地存放调用 receive()方法时应用程序协议所允许的最大长度的消息。一个 DatagramPacket 实例中所允许传输的最大数据量为 65507 个字节,也即是 UDP 数据报文所能负载的最多数据。因此,可以用一个 65600 字节左右的缓存数组来接受数据。

#####DatagramPacket 的内部消息长度值在接收数据后会发生改变,变为实际接收到的数据的长度值。

每一个 DatagramPacket 实例都包含一个内部消息长度值,其初始值为 byte 缓存数组的长度值,而该实例一旦接受到消息,这个长度值便会变为接收到的消息的实际长度值,这一点可以用 DatagramPacket 类的 getLength()方法来测试。如果一个应用程序使用同一个 DatagramPacket 实例多次调用 receive()方法,每次调用前就必须显式地将其内部消息长度重置为缓存区的实际长度,以免接受的数据发生丢失。

##练习

  1. 什么是端口号?服务器端和客户端分别如何使用端口号?
  2. 什么事套接字,其作用是什么?
  3. 编写一个可以查询网络域名的程序。
  4. 编写一个功能完善的聊天室程序。
  5. 编写一个客户/服务器程序,服务器端的功能是计算圆的面积。客户端将圆的半径发送给服务器,服务器端将计算得出的圆面积发送给客户端,并在客户端显示。

本文档 Github : https://github.com/bushehui/Java_tutorial

<script type="text/javascript" async src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML"> </script> <script type="text/x-mathjax-config"> MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]}}); </script>