网络编程基础(3)——TCP与UDP编程

13.2.3 TCP编程

         按照前面的介绍,网络通讯的方式有TCPUDP两种,其中TCP方式的网络通讯是指在通讯的过程中保持连接,有点类似于打电话,只需要拨打一次号码(建立一次网络连接),就可以多次通话(多次传输数据)。这样方式在实际的网络编程中,由于传输可靠,类似于打电话,如果甲给乙打电话,乙说没有听清楚让甲重复一遍,直到乙听清楚为止,实际的网络传输也是这样,如果发送的一方发送的数据接收方觉得有问题,则网络底层会自动要求发送方重发,直到接收方收到为止。

         Java语言中,对于TCP方式的网络编程提供了良好的支持,在实际实现时,以java.net.Socket类代表客户端连接,以java.net.ServerSocket类代表服务器端连接。在进行网络编程时,底层网络通讯的细节已经实现了比较高的封装,所以在程序员实际编程时,只需要指定IP地址和端口号码就可以建立连接了。正是由于这种高度的封装,一方面简化了Java语言网络编程的难度,另外也使得使用Java语言进行网络编程时无法深入到网络的底层,所以使用Java语言进行网络底层系统编程很困难,具体点说,Java语言无法实现底层的网络嗅探以及获得IP包结构等信息。但是由于Java语言的网络编程比较简单,所以还是获得了广泛的使用。

         在使用TCP方式进行网络编程时,需要按照前面介绍的网络编程的步骤进行,下面分别介绍一下在Java语言中客户端和服务器端的实现步骤。

在客户端网络编程中,首先需要建立连接,在Java API中以java.net.Socket类的对象代表网络连接,所以建立客户端网络连接,也就是创建Socket类型的对象,该对象代表网络连接,示例如下:

         Socket socket1 = new Socket(“192.168.1.103”,10000);

         Socket socket2 = new Socket(“www.sohu.com”,80);

上面的代码中,socket1实现的是连接到IP地址是192.168.1.103的计算机的10000号端口,而socket2实现的是连接到域名是www.sohu.com的计算机的80号端口,至于底层网络如何实现建立连接,对于程序员来说是完全透明的。如果建立连接时,本机网络不通,或服务器端程序未开启,则会抛出异常。

连接一旦建立,则完成了客户端编程的第一步,紧接着的步骤就是按照“请求-响应”模型进行网络数据交换,在Java语言中,数据传输功能由Java IO实现,也就是说只需要从连接中获得输入流和输出流即可,然后将需要发送的数据写入连接对象的输出流中,在发送完成以后从输入流中读取数据即可。示例代码如下:

         OutputStream os = socket1.getOutputStream(); //获得输出流

         InputStream is = socket1.getInputStream();     //获得输入流

上面的代码中,分别从socket1这个连接对象获得了输出流和输入流对象,在整个网络编程中,后续的数据交换就变成了IO操作,也就是遵循“请求-响应”模型的规定,先向输出流中写入数据,这些数据会被系统发送出去,然后在从输入流中读取服务器端的反馈信息,这样就完成了一次数据交换过程,当然这个数据交换过程可以多次进行。

这里获得的只是最基本的输出流和输入流对象,还可以根据前面学习到的IO知识,使用流的嵌套将这些获得到的基本流对象转换成需要的装饰流对象,从而方便数据的操作。

最后当数据交换完成以后,关闭网络连接,释放网络连接占用的系统端口和内存等资源,完成网络操作,示例代码如下:

         socket1.close();

这就是最基本的网络编程功能介绍。下面是一个简单的网络客户端程序示例,该程序的作用是向服务器端发送一个字符串“Hello”,并将服务器端的反馈显示到控制台,数据交换只进行一次,当数据交换进行完成以后关闭网络连接,程序结束。实现的代码如下:

package tcp;

import java.io.*;

import java.net.*;

/**

 * 简单的Socket客户端

 * 功能为:发送字符串“Hello”到服务器端,并打印出服务器端的反馈

 */

public class SimpleSocketClient {

         public static void main(String[] args) {

                   Socket socket = null;

                   InputStream is = null;

                   OutputStream os = null;

                   //服务器端IP地址

                   String serverIP = "127.0.0.1";

                   //服务器端端口号

                   int port = 10000;

                   //发送内容

                   String data = "Hello";

                   try {

                            //建立连接

                            socket = new Socket(serverIP,port);

                            //发送数据

                            os = socket.getOutputStream();

                            os.write(data.getBytes());

                            //接收数据

                            is = socket.getInputStream();

                            byte[] b = new byte[1024];

                            int n = is.read(b);

                            //输出反馈数据

                            System.out.println("服务器反馈:" + new String(b,0,n));

                   } catch (Exception e) {

                            e.printStackTrace(); //打印异常信息

                   }finally{

                            try {

                                     //关闭流和连接

                                     is.close();

                                     os.close();

                                     socket.close();

                            } catch (Exception e2) {}

                   }

         }

}

在该示例代码中建立了一个连接到IP地址为127.0.0.1,端口号码为10000TCP类型的网络连接,然后获得连接的输出流对象,将需要发送的字符串“Hello”转换为byte数组写入到输出流中,由系统自动完成将输出流中的数据发送出去,如果需要强制发送,可以调用输出流对象中的flush方法实现。在数据发送出去以后,从连接对象的输入流中读取服务器端的反馈信息,读取时可以使用IO中的各种读取方法进行读取,这里使用最简单的方法进行读取,从输入流中读取到的内容就是服务器端的反馈,并将读取到的内容在客户端的控制台进行输出,最后依次关闭打开的流对象和网络连接对象。

这是一个简单的功能示例,在该示例中演示了TCP类型的网络客户端基本方法的使用,该代码只起演示目的,还无法达到实用的级别。

如果需要在控制台下面编译和运行该代码,需要首先在控制台下切换到源代码所在的目录,然后依次输入编译和运行命令:

         javac –d . SimpleSocketClient.java

         java tcp.SimpleSocketClient

和下面将要介绍的SimpleSocketServer服务器端组合运行时,程序的输出结果为:

         服务器反馈:Hello

介绍完一个简单的客户端编程的示例,下面接着介绍一下TCP类型的服务器端的编写。首先需要说明的是,客户端的步骤和服务器端的编写步骤不同,所以在学习服务器端编程时注意不要和客户端混淆起来。

在服务器端程序编程中,由于服务器端实现的是被动等待连接,所以服务器端编程的第一个步骤是监听端口,也就是监听是否有客户端连接到达。实现服务器端监听的代码为:

         ServerSocket ss = new ServerSocket(10000);

该代码实现的功能是监听当前计算机的10000号端口,如果在执行该代码时,10000号端口已经被别的程序占用,那么将抛出异常。否则将实现监听。

服务器端编程的第二个步骤是获得连接。该步骤的作用是当有客户端连接到达时,建立一个和客户端连接对应的Socket连 接对象,从而释放客户端连接对于服务器端端口的占用。实现功能就像公司的前台一样,当一个客户到达公司时,会告诉前台我找某某某,然后前台就通知某某某, 然后就可以继续接待其它客户了。通过获得连接,使得客户端的连接在服务器端获得了保持,另外使得服务器端的端口释放出来,可以继续等待其它的客户端连接。 实现获得连接的代码是:

         Socket socket = ss.accept();

该代码实现的功能是获得当前连接到服务器端的客户端连接。需要说明的是accept和前面IO部分介绍的read方法一样,都是一个阻塞方法,也就是当无连接时,该方法将阻塞程序的执行,直到连接到达时才执行该行代码。另外获得的连接会在服务器端的该端口注册,这样以后就可以通过在服务器端的注册信息直接通信,而注册以后服务器端的端口就被释放出来,又可以继续接受其它的连接了。

连接获得以后,后续的编程就和客户端的网络编程类似了,这里获得的Socket类型的连接就和客户端的网络连接一样了,只是服务器端需要首先读取发送过来的数据,然后进行逻辑处理以后再发送给客户端,也就是交换数据的顺序和客户端交换数据的步骤刚好相反。这部分的内容和客户端很类似,所以就不重复了,如果还不熟悉,可以参看下面的示例代码。

最后,在服务器端通信完成以后,关闭服务器端连接。实现的代码为:

         ss.close();

这就是基本的TCP类型的服务器端编程步骤。下面以一个简单的echo服务实现为例子,介绍综合使用示例。echo的意思就是“回声”,echo服务器端实现的功能就是将客户端发送的内容再原封不动的反馈给客户端。实现的代码如下:

         package tcp;

import java.io.*;

import java.net.*;

/**

 * echo服务器

 * 功能:将客户端发送的内容反馈给客户端

 */

public class SimpleSocketServer {

         public static void main(String[] args) {

                   ServerSocket serverSocket = null;

                   Socket socket = null;

                   OutputStream os = null;

                   InputStream is = null;

                   //监听端口号

                   int port = 10000;

                   try {

                            //建立连接

                            serverSocket = new ServerSocket(port);

                            //获得连接

                            socket = serverSocket.accept();

                            //接收客户端发送内容

                            is = socket.getInputStream();

                            byte[] b = new byte[1024];

                            int n = is.read(b);

                            //输出

                            System.out.println("客户端发送内容为:" + new String(b,0,n));

                            //向客户端发送反馈内容

                            os = socket.getOutputStream();

                            os.write(b, 0, n);

                   } catch (Exception e) {

                            e.printStackTrace();

                   }finally{

                            try{

                                     //关闭流和连接

                                     os.close();

                                     is.close();

                                     socket.close();

                                     serverSocket.close();

                            }catch(Exception e){}

                   }

         }

}

在该示例代码中建立了一个监听当前计算机10000号端口的服务器端Socket连接,然后获得客户端发送过来的连接,如果有连接到达时,读取连接中发送过来的内容,并将发送的内容在控制台进行输出,输出完成以后将客户端发送的内容再反馈给客户端。最后关闭流和连接对象,结束程序。

在控制台下面编译和运行该程序的命令和客户端部分的类似。

这样,就以一个很简单的示例演示了TCP类型的网络编程在Java语言中的基本实现,这个示例只是演示了网络编程的基本步骤以及各个功能方法的基本使用,只是为网络编程打下了一个基础,下面将就几个问题来深入介绍网络编程深层次的一些知识。

 为了一步一步的掌握网络编程,下面再研究网络编程中的两个基本问题,通过解决这两个问题将对网络编程的认识深入一层。

1、如何复用Socket连接?

在前面的示例中,客户端中建立了一次连接,只发送一次数据就关闭了,这就相当于拨打电话时,电话打通了只对话一次就关闭了,其实更加常用的应该是拨通一次电话以后多次对话,这就是复用客户端连接。

那 么如何实现建立一次连接,进行多次数据交换呢?其实很简单,建立连接以后,将数据交换的逻辑写到一个循环中就可以了。这样只要循环不结束则连接就不会被关 闭。按照这种思路,可以改造一下上面的代码,让该程序可以在建立连接一次以后,发送三次数据,当然这里的次数也可以是多次,示例代码如下:

package tcp;

import java.io.*;

import java.net.*;

/**

 * 复用连接的Socket客户端

 * 功能为:发送字符串“Hello”到服务器端,并打印出服务器端的反馈

 */

public class MulSocketClient {

         public static void main(String[] args) {

                   Socket socket = null;

                   InputStream is = null;

                   OutputStream os = null;

                   //服务器端IP地址

                   String serverIP = "127.0.0.1";

                   //服务器端端口号

                   int port = 10000;

                   //发送内容

                   String data[] ={"First","Second","Third"};

                   try {

                            //建立连接

                            socket = new Socket(serverIP,port);

                            //初始化流

                            os = socket.getOutputStream();

                            is = socket.getInputStream();

                            byte[] b = new byte[1024];

                            for(int i = 0;i < data.length;i++){

                                     //发送数据

                                     os.write(data[i].getBytes());

                                     //接收数据

                                     int n = is.read(b);

                                     //输出反馈数据

                                     System.out.println("服务器反馈:" + new String(b,0,n));

                            }

                   } catch (Exception e) {

                            e.printStackTrace(); //打印异常信息

                   }finally{

                            try {

                                     //关闭流和连接

                                     is.close();

                                     os.close();

                                     socket.close();

                            } catch (Exception e2) {}

                   }

         }

}

该示例程序和前面的代码相比,将数据交换部分的逻辑写在一个for循环的内容,这样就可以建立一次连接,依次将data数组中的数据按照顺序发送给服务器端了。

                   如果还是使用前面示例代码中的服务器端程序运行该程序,则该程序的结果是:

                            java.net.SocketException: Software caused connection abort: recv failed

                                     at java.net.SocketInputStream.socketRead0(Native Method)

                                     at java.net.SocketInputStream.read(SocketInputStream.java:129)

                                     at java.net.SocketInputStream.read(SocketInputStream.java:90)

                                     at tcp.MulSocketClient.main(MulSocketClient.java:30)

服务器反馈:First

显然,客户端在实际运行时出现了异常,出现异常的原因是什么呢?如果仔细阅读前面的代码,应该还记得前面示例代码中的服务器端是对话一次数据以后就关闭了连接,如果服务器端程序关闭了,客户端继续发送数据肯定会出现异常,这就是出现该问题的原因。

按照客户端实现的逻辑,也可以复用服务器端的连接,实现的原理也是将服务器端的数据交换逻辑写在循环中即可,按照该种思路改造以后的服务器端代码为:

         package tcp;

import java.io.*;

import java.net.*;

/**

 * 复用连接的echo服务器

 * 功能:将客户端发送的内容反馈给客户端

 */

public class MulSocketServer {

         public static void main(String[] args) {

                   ServerSocket serverSocket = null;

                   Socket socket = null;

                   OutputStream os = null;

                   InputStream is = null;

                   //监听端口号

                   int port = 10000;

                   try {

                            //建立连接

                            serverSocket = new ServerSocket(port);

                            System.out.println("服务器已启动:");

                            //获得连接

                            socket = serverSocket.accept();

                            //初始化流

                            is = socket.getInputStream();

                            os = socket.getOutputStream();

                            byte[] b = new byte[1024];

                            for(int i = 0;i < 3;i++){

                                     int n = is.read(b);

                                     //输出

                                     System.out.println("客户端发送内容为:" + new String(b,0,n));

                                     //向客户端发送反馈内容

                                     os.write(b, 0, n);

                            }

                   } catch (Exception e) {

                            e.printStackTrace();

                   }finally{

                            try{

                                     //关闭流和连接

                                     os.close();

                                     is.close();

                                     socket.close();

                                     serverSocket.close();

                            }catch(Exception e){}

                   }

         }

}

在该示例代码中,也将数据发送和接收的逻辑写在了一个for循环内部,只是在实现时硬性的将循环次数规定成了3次,这样代码虽然比较简单,但是通用性比较差。

                   以该服务器端代码实现为基础运行前面的客户端程序时,客户端的输出为:

                            服务器反馈:First

服务器反馈:Second

服务器反馈:Third

       服务器端程序的输出结果为:

           服务器已启动:

客户端发送内容为:First

客户端发送内容为:Second

客户端发送内容为:Third

在该程序中,比较明显的体现出了“请求-响应”模型,也就是在客户端发起连接以后,首先发送字符串“First”给服务器端,服务器端输出客户端发送的内容“First”,然后将客户端发送的内容再反馈给客户端,这样客户端也输出服务器反馈“First”,这样就完成了客户端和服务器端的一次对话,紧接着客户端发送“Second”给服务器端,服务端输出“Second”,然后将“Second”再反馈给客户端,客户端再输出“Second”,从而完成第二次会话,第三次会话的过程和这个一样。在这个过程中,每次都是客户端程序首先发送数据给服务器端,服务器接收数据以后,将结果反馈给客户端,客户端接收到服务器端的反馈,从而完成一次通讯过程。

在该示例中,虽然解决了多次发送的问题,但是客户端和服务器端的次数控制还不够灵活,如果客户端的次数不固定怎么办呢?是否可以使用某个特殊的字符串,例如quit,表示客户端退出呢,这就涉及到网络协议的内容了,会在后续的网络应用示例部分详细介绍。下面开始介绍另外一个网络编程的突出问题。

2、如何使服务器端支持多个客户端同时工作?

         前面介绍的服务器端程序,只是实现了概念上的服务器端,离实际的服务器端程序结构距离还很遥远,如果需要让服务器端能够实际使用,那么最需要解决的问题就是——如何支持多个客户端同时工作。

         一个服务器端一般都需要同时为多个客户端提供通讯,如果需要同时支持多个客户端,则必须使用前面介绍的线程的概念。简单来说,也就是当服务器端接收到一个连接时,启动一个专门的线程处理和该客户端的通讯。

         按照这个思路改写的服务端示例程序将由两个部分组成,MulThreadSocketServer类实现服务器端控制,实现接收客户端连接,然后开启专门的逻辑线程处理该连接,LogicThread类实现对于一个客户端连接的逻辑处理,将处理的逻辑放置在该类的run方法中。该示例的代码实现为:

                   package tcp;

import java.net.ServerSocket;

import java.net.Socket;

/**

 * 支持多客户端的服务器端实现

 */

public class MulThreadSocketServer {

         public static void main(String[] args) {

                   ServerSocket serverSocket = null;

                   Socket socket = null;

                   //监听端口号

                   int port = 10000;

                   try {

                            //建立连接

                            serverSocket = new ServerSocket(port);

                            System.out.println("服务器已启动:");

                            while(true){

                                     //获得连接

                                     socket = serverSocket.accept();

                                     //启动线程

                                     new LogicThread(socket);

                            }

                   } catch (Exception e) {

                            e.printStackTrace();

                   }finally{

                            try{

                                     //关闭连接

                                     serverSocket.close();

                            }catch(Exception e){}

                   }

         }

}

         在该示例代码中,实现了一个while形式的死循环,由于accept方法是阻塞方法,所以当客户端连接未到达时,将阻塞该程序的执行,当客户端到达时接收该连接,并启动一个新的LogicThread线程处理该连接,然后按照循环的执行流程,继续等待下一个客户端连接。这样当任何一个客户端连接到达时,都开启一个专门的线程处理,通过多个线程支持多个客户端同时处理。

         下面再看一下LogicThread线程类的源代码实现:

                   package tcp;

import java.io.*;

import java.net.*;

/**

 * 服务器端逻辑线程

 */

public class LogicThread extends Thread {

         Socket socket;

         InputStream is;

         OutputStream os;

         public LogicThread(Socket socket){

                   this.socket = socket;

                   start(); //启动线程

         }

        

         public void run(){

                   byte[] b = new byte[1024];

                   try{

                            //初始化流

                            os = socket.getOutputStream();

                            is = socket.getInputStream();

                            for(int i = 0;i < 3;i++){

                                     //读取数据

                                     int n = is.read(b);

                                     //逻辑处理

                                     byte[] response = logic(b,0,n);

                                     //反馈数据

                                     os.write(response);

                            }

                   }catch(Exception e){

                            e.printStackTrace();

                   }finally{

                            close();

                   }

         }

        

         /**

          * 关闭流和连接

          */

         private void close(){

                   try{

                            //关闭流和连接

                            os.close();

                            is.close();

                            socket.close();

                   }catch(Exception e){}

         }

        

         /**

          * 逻辑处理方法,实现echo逻辑

          * @param b 客户端发送数据缓冲区

          * @param off 起始下标

          * @param len 有效数据长度

          * @return

          */

         private byte[] logic(byte[] b,int off,int len){

                   byte[] response = new byte[len];

                   //将有效数据拷贝到数组response

                   System.arraycopy(b, 0, response, 0, len);

                   return response;

         }

}

         在该示例代码中,每次使用一个连接对象构造该线程,该连接对象就是该线程需要处理的连接,在线程构造完成以后,该线程就被启动起来了,然后在run方法内部对客户端连接进行处理,数据交换的逻辑和前面的示例代码一致,只是这里将接收到客户端发送过来的数据并进行处理的逻辑封装成了logic方法,按照前面介绍的IO编程的内容,客户端发送过来的内容存储在数组b的起始下标为0,长度为n个中,这些数据是客户端发送过来的有效数据,将有效的数据传递给logic方法,logic方法实现的是echo服务的逻辑,也就是将客户端发送的有效数据形成以后新的response数组,并作为返回值反馈。

         在线程中将logic方法的返回值反馈给客户端,这样就完成了服务器端的逻辑处理模拟,其他的实现和前面的介绍类似,这里就不在重复了。

         这里的示例还只是基础的服务器端实现,在实际的服务器端实现中,由于硬件和端口数的限制,所以不能无限制的创建线程对象,而且频繁的创建线程对象效率也比较低,所以程序中都实现了线程池来提高程序的执行效率。

         这里简单介绍一下线程池的概念,线程池(Thread pool)是池技术的一种,就是在程序启动时首先把需要个数的线程对象创建好,例如创建5000个线程对象,然后当客户端连接到达时从池中取出一个已经创建完成的线程对象使用即可。当客户端连接关闭以后,将该线程对象重新放入到线程池中供其它的客户端重复使用,这样可以提高程序的执行速度,优化程序对于内存的占用等。

         关于基础的TCP方式的网络编程就介绍这么多,下面介绍UDP方式的网络编程在Java语言中的实现。

 

 

 

       网络通讯的方式除了TCP方式以外,还有一种实现的方式就是UDP方式。UDP(User Datagram Protocol),中文意思是用户数据报协议,方式类似于发短信息,是一种物美价廉的通讯方式,使用该种方式无需建立专用的虚拟连接,由于无需建立专用的连接,所以对于服务器的压力要比TCP小很多,所以也是一种常见的网络编程方式。但是使用该种方式最大的不足是传输不可靠,当然也不是说经常丢失,就像大家发短信息一样,理论上存在收不到的可能,这种可能性可能是1%,反正比较小,但是由于这种可能的存在,所以平时我们都觉得重要的事情还是打个电话吧(类似TCP方式),一般的事情才发短信息(类似UDP方式)。网络编程中也是这样,必须要求可靠传输的信息一般使用TCP方式实现,一般的数据才使用UDP方式实现。

         UDP方式的网络编程也在Java语言中获得了良好的支持,由于其在传输数据的过程中不需要建立专用的连接等特点,所以在Java API中设计的实现结构和TCP方式不太一样。当然,需要使用的类还是包含在java.net包中。

         Java API中,实现UDP方式的编程,包含客户端网络编程和服务器端网络编程,主要由两个类实现,分别是:

DatagramSocket

DatagramSocket类实现网络连接,包括客户端网络连接和服务器端网络连接。虽然UDP方式的网络通讯不需要建立专用的网络连接,但是毕竟还是需要发送和接收数据,DatagramSocket实现的就是发送数据时的发射器,以及接收数据时的监听器的角色。类比于TCP中的网络连接,该类既可以用于实现客户端连接,也可以用于实现服务器端连接。

DatagramPacket

DatagramPacket类实现对于网络中传输的数据封装,也就是说,该类的对象代表网络中交换的数据。在UDP方式的网络编程中,无论是需要发送的数据还是需要接收的数据,都必须被处理成DatagramPacket类型的对象,该对象中包含发送到的地址、发送到的端口号以及发送的内容等。其实DatagramPacket类的作用类似于现实中的信件,在信件中包含信件发送到的地址以及接收人,还有发送的内容等,邮局只需要按照地址传递即可。在接收数据时,接收到的数据也必须被处理成DatagramPacket类型的对象,在该对象中包含发送方的地址、端口号等信息,也包含数据的内容。和TCP方式的网络传输相比,IO编程在UDP方式的网络编程中变得不是必须的内容,结构也要比TCP方式的网络编程简单一些。

         下面介绍一下UDP方式的网络编程中,客户端和服务器端的实现步骤,以及通过基础的示例演示UDP方式的网络编程在Java语言中的实现方式。

         UDP方式的网络编程,编程的步骤和TCP方式类似,只是使用的类和方法存在比较大的区别,下面首先介绍一下UDP方式的网络编程客户端实现过程。

UDP客户端编程涉及的步骤也是4个部分:建立连接、发送数据、接收数据和关闭连接。

首先介绍UDP方式的网络编程中建立连接的实现。其中UDP方式的建立连接和TCP方式不同,只需要建立一个连接对象即可,不需要指定服务器的IP和端口号码。实现的代码为:

                   DatagramSocket ds = new DatagramSocket();

         这样就建立了一个客户端连接,该客户端连接使用系统随机分配的一个本地计算机的未用端口号。在该连接中,不指定服务器端的IP和端口,所以UDP方式的网络连接更像一个发射器,而不是一个具体的连接。

         当然,可以通过制定连接使用的端口号来创建客户端连接。

                   DatagramSocket ds = new DatagramSocket(5000);

         这样就是使用本地计算机的5000号端口建立了一个连接。一般在建立客户端连接时没有必要指定端口号码。

         接着,介绍一下UDP客户端编程中发送数据的实现。在UDP方式的网络编程中,IO技术不是必须的,在发送数据时,需要将需要发送的数据内容首先转换为byte数组,然后将数据内容、服务器IP和服务器端口号一起构造成一个DatagramPacket类型的对象,这样数据的准备就完成了,发送时调用网络连接对象中的send方法发送该对象即可。例如将字符串“Hello”发送到IP127.0.0.1,端口号是10001的服务器,则实现发送数据的代码如下:

                  String s = “Hello”;

                   String host = “127.0.0.1”;

                   int port = 10001;

                  //将发送的内容转换为byte数组

                   byte[] b = s.getBytes();

                   //将服务器IP转换为InetAddress对象

                   InetAddress server = InetAddress.getByName(host);

                   //构造发送的数据包对象

                   DatagramPacket sendDp = new DatagramPacket(b,b.length,server,port);

                   //发送数据

                   ds.send(sendDp);

         在该示例代码中,不管发送的数据内容是什么,都需要转换为byte数组,然后将服务器端的IP地址构造成InetAddress类型的对象,在准备完成以后,将这些信息构造成一个DatagramPacket类型的对象,在UDP编程中,发送的数据内容、服务器端的IP和端口号,都包含在DatagramPacket对象中。在准备完成以后,调用连接对象dssend方法把DatagramPacket对象发送出去即可。

         按照UDP协议的约定,在进行数据传输时,系统只是尽全力传输数据,但是并不保证数据一定被正确传输,如果数据在传输过程中丢失,那就丢失了。

         UDP方式在进行网络通讯时,也遵循“请求-响应”模型,在发送数据完成以后,就可以接收服务器端的反馈数据了。

         下面介绍一下UDP客户端编程中接收数据的实现。当数据发送出去以后,就可以接收服务器端的反馈信息了。接收数据在Java语言中的实现是这样的:首先构造一个数据缓冲数组,该数组用于存储接收的服务器端反馈数据,该数组的长度必须大于或等于服务器端反馈的实际有效数据的长度。然后以该缓冲数组为基础构造一个DatagramPacket数据包对象,最后调用连接对象的receive方法接收数据即可。接收到的服务器端反馈数据存储在DatagramPacket类型的对象内部。实现接收数据以及显示服务器端反馈内容的示例代码如下:

                   //构造缓冲数组

                   byte[] data = new byte[1024];

                   //构造数据包对象

                   DatagramPacket received = new DatagramPacket(data,data.length);

                   //接收数据

                   ds.receive(receiveDp);

                   //输出数据内容

                   byte[] b = receiveDp.getData(); //获得缓冲数组

                   int len = receiveDp.getLength(); //获得有效数据长度

                   String s = new String(b,0,len);

                   System.out.println(s);

         在该代码中,首先构造缓冲数组data,这里设置的长度1024是预估的接收到的数据长度,要求该长度必须大于或等于接收到的数据长度,然后以该缓冲数组为基础,构造数据包对象,使用连接对象dsreceive方法接收反馈数据,由于在Java语言中,除String以外的其它对象都是按照地址传递,所以在receive方法内部可以改变数据包对象receiveDp的内容,这里的receiveDp的功能和返回值类似。数据接收到以后,只需要从数据包对象中读取出来就可以了,使用DatagramPacket对象中的getData方法可以获得数据包对象的缓冲区数组,但是缓冲区数组的长度一般大于有效数据的长度,换句话说,也就是缓冲区数组中只有一部分数据是反馈数据,所以需要使用DatagramPacket对象中的getLength方法获得有效数据的长度,则有效数据就是缓冲数组中的前有效数据长度个内容,这些才是真正的服务器端反馈的数据的内容。

         UDP方式客户端网络编程的最后一个步骤就是关闭连接。虽然UDP方式不建立专用的虚拟连接,但是连接对象还是需要占用系统资源,所以在使用完成以后必须关闭连接。关闭连接使用连接对象中的close方法即可,实现的代码如下:

                   ds.close();

         需要说明的是,和TCP建立连接的方式不同,UDP方式的同一个网络连接对象,可以发送到达不同服务器端IP或端口的数据包,这点是TCP方式无法做到的。

         介绍完了UDP方式客户端网络编程的基础知识以后,下面再来介绍一下UDP方式服务器端网络编程的基础知识。

         UDP方式网络编程的服务器端实现和TCP方式的服务器端实现类似,也是服务器端监听某个端口,然后获得数据包,进行逻辑处理以后将处理以后的结果反馈给客户端,最后关闭网络连接,下面依次进行介绍。

         首先UDP方式服务器端网络编程需要建立一个连接,该连接监听某个端口,实现的代码为:

           DatagramSocket ds = new DatagramSocket(10010);

由于服务器端的端口需要固定,所以一般在建立服务器端连接时,都指定端口号。例如该示例代码中指定10010端口为服务器端使用的端口号,客户端端在连接服务器端时连接该端口号即可。

接着服务器端就开始接收客户端发送过来的数据,其接收的方法和客户端接收的方法一直,其中receive方法的作用类似于TCP方式中accept方法的作用,该方法也是一个阻塞方法,其作用是接收数据。

接收到客户端发送过来的数据以后,服务器端对该数据进行逻辑处理,然后将处理以后的结果再发送给客户端,在这里发送时就比客户端要麻烦一些,因为服务器端需要获得客户端的IP和客户端使用的端口号,这个都可以从接收到的数据包中获得。示例代码如下:

     //获得客户端的IP

     InetAddress clientIP = receiveDp.getAddress();

         //获得客户端的端口号

         Int clientPort = receiveDp.getPort();

     使用以上代码,就可以从接收到的数据包对象receiveDp中获得客户端的IP地址和客户端的端口号,这样就可以在服务器端中将处理以后的数据构造成数据包对象,然后将处理以后的数据内容反馈给客户端了。

     最后,当服务器端实现完成以后,关闭服务器端连接,实现的方式为调用连接对象的close方法,示例代码如下:

         ds.close();

     介绍完了UDP方式下的客户端编程和服务器端编程的基础知识以后,下面通过一个简单的示例演示UDP网络编程的基本使用。

 

该示例的功能是实现将客户端程序的系统时间发送给服务器端,服务器端接收到时间以后,向客户端反馈字符串“OK”。实现该功能的客户端代码如下所示:

        package udp;

import java.net.*;

import java.util.*;

/**

 * 简单的UDP客户端,实现向服务器端发生系统时间功能

 */

public class SimpleUDPClient {

            public static void main(String[] args) {

                     DatagramSocket ds = null; //连接对象

                     DatagramPacket sendDp; //发送数据包对象

                DatagramPacket receiveDp; //接收数据包对象

                     String serverHost = "127.0.0.1"; //服务器IP

                int serverPort = 10010; //服务器端口号

                     try{

                        //建立连接

                        ds = new DatagramSocket();

                        //初始化发送数据

                        Date d = new Date(); //当前时间

                        String content = d.toString(); //转换为字符串

                        byte[] data = content.getBytes();

                        //初始化发送包对象

                        InetAddress address = InetAddress.getByName(serverHost);

                        sendDp = new DatagramPacket(data,data.length,address,serverPort);

                        //发送

                        ds.send(sendDp);

                                                                          

                        //初始化接收数据

                        byte[] b = new byte[1024];

                        receiveDp = new DatagramPacket(b,b.length);

                        //接收

                        ds.receive(receiveDp);

                        //读取反馈内容,并输出

                        byte[] response = receiveDp.getData();

                        int len = receiveDp.getLength();

                        String s = new String(response,0,len);

                        System.out.println("服务器端反馈为:" + s);

                }catch(Exception e){

                        e.printStackTrace();

                }finally{

                        try{

                           //关闭连接

                           ds.close();

                        }catch(Exception e){}

                }

            }

        }

在该示例代码中,首先建立UDP方式的网络连接,然后获得当前系统时间,这里获得的系统时间是客户端程序运行的本地计算机的时间,然后将时间字符串以及服务器端的IP和端口,构造成发送数据包对象,调用连接对象dssend方法发送出去。在数据发送出去以后,构造接收数据的数据包对象,调用连接对象dsreceive方法接收服务器端的反馈,并输出在控制台。最后在finally语句块中关闭客户端网络连接。

和下面将要介绍的服务器端一起运行时,客户端程序的输出结果为:

    服务器端反馈为:OK

下面是该示例程序的服务器端代码实现:

       package udp;

        import java.net.*;

        /**

         * 简单UDP服务器端,实现功能是输出客户端发送数据,

           并反馈字符串“OK"给客户端

         */

        public class SimpleUDPServer {

            public static void main(String[] args) {

                     DatagramSocket ds = null; //连接对象

                     DatagramPacket sendDp; //发送数据包对象

                     DatagramPacket receiveDp; //接收数据包对象

                     final int PORT = 10010; //端口

                                               try{

                        //建立连接,监听端口

                        ds = new DatagramSocket(PORT);

                       System.out.println("服务器端已启动:");

                        //初始化接收数据

                        byte[] b = new byte[1024];

                        receiveDp = new DatagramPacket(b,b.length);

                        //接收

                        ds.receive(receiveDp);

                        //读取反馈内容,并输出

                        InetAddress clientIP = receiveDp.getAddress();

                        int clientPort = receiveDp.getPort();

                        byte[] data = receiveDp.getData();

                        int len = receiveDp.getLength();

                        System.out.println("客户端IP" + clientIP.getHostAddress());

                        System.out.println("客户端端口:" + clientPort);

                        System.out.println("客户端发送内容:" + new String(data,0,len));

                                                                          

                        //发送反馈

                        String response = "OK";

                        byte[] bData = response.getBytes();

                        sendDp = new DatagramPacket(bData,bData.length,clientIP,clientPort);

                        //发送

                        ds.send(sendDp);

                                               }catch(Exception e){

                        e.printStackTrace();

                                               }finally{

                        try{

                           //关闭连接

                           ds.close();

                        }catch(Exception e){}

                                               }

            }

        }

在该服务器端实现中,首先监听10010号端口,和TCP方式的网络编程类似,服务器端的receive方法是阻塞方法,如果客户端不发送数据,则程序会在该方法处阻塞。当客户端发送数据到达服务器端时,则接收客户端发送过来的数据,然后将客户端发送的数据内容读取出来,并在服务器端程序中打印客户端的相关信息,从客户端发送过来的数据包中可以读取出客户端的IP以及客户端端口号,将反馈数据字符串“OK”发送给客户端,最后关闭服务器端连接,释放占用的系统资源,完成程序功能示例。

和前面TCP方式中的网络编程类似,这个示例也仅仅是网络编程的功能示例,也存在前面介绍的客户端无法进行多次数据交换,以及服务器端不支持多个客户端的问题,这两个问题也需要对于代码进行处理才可以很方便的进行解决。

在解决该问题以前,需要特别指出的是UDP方式的网络编程由于不建立虚拟的连接,所以在实际使用时和TCP方式存在很多的不同,最大的一个不同就是“无状态”。该特点指每次服务器端都收到信息,但是这些信息和连接无关,换句话说,也就是服务器端只是从信息是无法识别出是谁发送的,这样就要求发送信息时的内容需要多一些,这个在后续的示例中可以看到。

下面是实现客户端多次发送以及服务器端支持多个数据包同时处理的程序结构,实现的原理和TCP方式类似,在客户端将数据的发送和接收放入循环中,而服务器端则将接收到的每个数据包启动一个专门的线程进行处理。实现的代码如下:

    package udp;

    import java.net.*;

    import java.util.*;

    /**

     * 简单的UDP客户端,实现向服务器端发生系统时间功能

     * 该程序发送3次数据到服务器端

     */

    public class MulUDPClient {

                  public static void main(String[] args) {

                 DatagramSocket ds = null; //连接对象

                                     DatagramPacket sendDp; //发送数据包对象

                                     DatagramPacket receiveDp; //接收数据包对象

                                     String serverHost = "127.0.0.1"; //服务器IP

                                     int serverPort = 10012; //服务器端口号

                                     try{

                    //建立连接

                    ds = new DatagramSocket();

                    //初始化

                              InetAddress address = InetAddress.getByName(serverHost);

                    byte[] b = new byte[1024];

                    receiveDp = new DatagramPacket(b,b.length);

                    System.out.println("客户端准备完成");

                    //循环30次,每次间隔0.01

                    for(int i = 0;i < 30;i++){

                                                        //初始化发送数据

                                                        Date d = new Date(); //当前时间

                                                        String content = d.toString(); //转换为字符串

                                                        byte[] data = content.getBytes();

                                                        //初始化发送包对象

                                                        sendDp = new DatagramPacket(data,data.length,address, serverPort);

                                                        //发送

                                                        ds.send(sendDp);

                                                        //延迟

                                                        Thread.sleep(10);

                                                        //接收

                                                        ds.receive(receiveDp);

                                                        //读取反馈内容,并输出

                                                        byte[] response = receiveDp.getData();

                                                        int len = receiveDp.getLength();

                                                        String s = new String(response,0,len);

                                                        System.out.println("服务器端反馈为:" + s);

                     }

                 }catch(Exception e){

                     e.printStackTrace();

                 }finally{

                     try{

                                                        //关闭连接

                                                        ds.close();

                     }catch(Exception e){}

                 }

         }

     }

在该示例中,将和服务器端进行数据交换的逻辑写在一个for循环的内部,这样就可以实现和服务器端的多次交换了,考虑到服务器端的响应速度,在每次发送之间加入0.01秒的时间间隔。最后当数据交换完成以后关闭连接,结束程序。

实现该逻辑的服务器端程序代码如下:

package udp;

import java.net.*;

/**

可以并发处理数据包的服务器端

功能为:显示客户端发送的内容,并向客户端反馈字符串“OK

*/

public class MulUDPServer {

public static void main(String[] args) {

DatagramSocket ds = null; //连接对象

DatagramPacket receiveDp; //接收数据包对象

final int PORT = 10012; //端口

byte[] b = new byte[1024];

receiveDp = new DatagramPacket(b,b.length);

try{

//建立连接,监听端口

ds = new DatagramSocket(PORT);

System.out.println("服务器端已启动:");

while(true){

//接收

ds.receive(receiveDp);

//启动线程处理数据包

new LogicThread(ds,receiveDp);

}

}catch(Exception e){

         e.printStackTrace();

}finally{

try{

//关闭连接

ds.close();

}catch(Exception e){}

}

}

}

该代码实现了服务器端的接收逻辑,使用一个循环来接收客户端发送过来的数据包,当接收到数据包以后启动一个LogicThread线程处理该数据包。这样服务器端就可以实现同时处理多个数据包了。

实现逻辑处理的线程代码如下:

package udp;

import java.net.*;

/**

 * 逻辑处理线程

 */

public class LogicThread extends Thread {

/**连接对象*/

DatagramSocket ds;

/**接收到的数据包*/

DatagramPacket dp;

 

public LogicThread(DatagramSocket ds,DatagramPacket dp){

this.ds = ds;

this.dp = dp;

start(); //启动线程

}

 

public void run(){

try{

//获得缓冲数组

byte[] data = dp.getData();

//获得有效数据长度

int len = dp.getLength();

//客户端IP

InetAddress clientAddress = dp.getAddress();

//客户端端口

int clientPort = dp.getPort();

//输出

System.out.println("客户端IP" + clientAddress.getHostAddress());

System.out.println("客户端端口号:" + clientPort);

System.out.println("客户端发送内容:" + new String(data,0,len));

//反馈到客户端

byte[] b = "OK".getBytes();

DatagramPacket sendDp = new DatagramPacket(b,b.length,clientAddress,clientPort);

//发送

ds.send(sendDp);

}catch(Exception e){

e.printStackTrace();

}

}

}

在该线程中,只处理一次UDP通讯,当通讯结束以后线程死亡,在线程内部,每次获得客户端发送过来的信息,将获得的信息输出到服务器端程序的控制台,然后向客户端反馈字符串“OK”。

由于UDP数据传输过程中可能存在丢失,所以在运行该程序时可能会出现程序阻塞的情况。如果需要避免该问题,可以将客户端的网络发送部分也修改成线程实现。

关于基础的UDP网络编程就介绍这么多了,下面将介绍一下网络协议的概念。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值