在现有的网络中,网络通讯的方式主要有两种:

  1.  TCP(传输控制协议)方式
  2.  UDP(用户数据报协议)方式

 在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。

 而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。

 这两种传输方式都是实际的网络编程中进行使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则都通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据的传递。

 由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。

 无论使用TCP方式还是UDP方式进行网络通讯,网络编程都是由客户端和服务器端组成当然,B/S结构的编程中只需要实现服务器端即可。所以,下面介绍网络编程的步骤时,均以C/S结构为基础进行介绍。


 

 

网络编程技术

1、客户端网络编程步骤

客户端(Client)是指网络编程中首先发起连接的程序,客户端一般实现程序界面和基本逻辑实现,在进行实际的客户端编程时,无论客户端复杂还是简单,以及客户端实现的方式,客户端的编程主要由三个步骤实现:

  • 建立网络连接
     客户端网络编程的第一步都是建立网络连接。在建立网络连接时需要指定连接到的服务器的IP地址和端口号,建立完成以后,会形成一条虚拟的连接,后续的操作就可以通过该连接实现数据交换了。
  • 交换数据
     连接建立以后,就可以通过这个连接交换数据了。交换数据严格按照请求响应模型进行,由客户端发送一个请求数据到服务器,服务器反馈一个响应数据给客户端,如果客户端不发送请求则服务器端就不响应。根据逻辑需要,可以多次交换数据,但是还是必须遵循请求响应模型。
  • 关闭网络连接
     在数据交换完成以后,关闭网络连接,释放程序占用的端口、内存等系统资源,结束网络编程。

 

在实际实现时,步骤2会出现重复,在进行代码组织时,由于网络编程是比较耗时的操作,所以一般开启专门的现场进行网络通讯。

 

2、服务器端网络编程步骤

  服务器端(Server)是指在网络编程中被动等待连接的程序,服务器端一般实现程序的核心逻辑以及数据存储等核心功能。服务器端的编程步骤和客户端不同,是由四个步骤实现,依次是:

  • 监听端口

     服务器端属于被动等待连接,所以服务器端启动以后,不需要发起连接,而只需要监听本地计算机的某个固定端口即可。

     这个端口就是服务器端开放给客户端的端口,服务器端程序运行的本地计算机的IP地址就是服务器端程序的IP地址。

  • 获得连接

     当客户端连接到服务器端时,服务器端就可以获得一个连接,这个连接包含客户端的信息,例如客户端IP地址等等,服务器端和客户端也通过该连接进行数据交换。

     一般在服务器端编程中,当获得连接时,需要开启专门的线程处理该连接,每个连接都由独立的线程实现。

  • 换数据

     服务器端通过获得的连接进行数据交换。服务器端的数据交换步骤是首先接收客户端发送过来的数据,然后进行逻辑处理,再把处理以后的结果数据发送给客户端。简单来说,就是先接收再发送,这个和客户端的数据交换数序不同。

     其实,服务器端获得的连接和客户端连接是一样的,只是数据交换的步骤不同。

     当然,服务器端的数据交换也是可以多次进行的。

     在数据交换完成以后,关闭和客户端的连接。

  • 关闭连接
     当服务器程序关闭时,需要关闭服务器端,通过关闭服务器端使得服务器监听的端口以及占用的内存可以释放出来,实现了连接的关闭。

TCP方式是需要建立连接的,对于服务器端的压力比较大,而UDP是不需要建立连接的,对于服务器端的压力比较小罢了。


 

 

 Java网络编程技术

  和网络编程有关的基本API位于java.net包中,该包中包含了基本的网络编程实现,该包是网络编程的基础。该包中既包含基础的网络编程类,也包含封装后的专门处理WEB相关的处理类。

InetAddress

  该类的功能是代表一个IP地址,并且将IP地址和域名相关的操作方法包含在该类的内部。

   先来个Demo

 
  1. public static void main(String[] args) throws IOException { 
  2.  
  3.         try { 
  4.  
  5.             //使用域名创建对象 
  6.             InetAddress address=InetAddress.getByName("www.163.com"); 
  7.             System.out.println(address); 
  8.              
  9.             //使用ip创建对象 
  10.             InetAddress address2=InetAddress.getByName("222.184.115.167"); 
  11.             System.out.println(address2); 
  12.              
  13.             //获得本机地址对象 
  14.             InetAddress address3 = InetAddress.getLocalHost(); 
  15.             System.out.println(address3); 
  16.              
  17.             //获得对象中存储的域名 
  18.             System.out.println("域名:"+address3.getHostName()); 
  19.              
  20.             //获得对象中存储的ip地址 
  21.             System.out.println("IP地址:"+address3.getHostAddress()); 
  22.         } catch (Exception e) { 
  23.             // TODO: handle exception 
  24.         } 
  25.  
  26.     } 

 由于该代码中包含一个互联网的网址,所以运行该程序时需要联网,否则将产生异常。

 在后续的使用中,经常包含需要使用InetAddress对象代表IP地址的构造方法,当然,该类的使用不是必须的,也可以使用字符串来代表IP地址进行实现。


 

TCP编程

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

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

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

 
  1. // socket1实现的是连接到IP地址是192.168.1.103的计算机的10000号端口 
  2. Socket socket1 = new Socket("192.168.1.103"10000); 
  3. // socket2实现的是连接到域名是www.sohu.com的计算机的80号端口 
  4. Socket socket2 = new Socket("www.sohu.com"80); 

  底层网络如何实现建立连接,对于程序员来说是完全透明的。如果建立连接时,本机网络不通,或服务器端程序未开启,则会抛出异常。

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

 
  1. //获得输出流 
  2. OutputStream outputStream = socket1.getOutputStream(); 
  3. //获得输入流 
  4. InputStream inputStream=socket1.getInputStream(); 

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

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

 
  1. socket1.close(); 

以上就是最基本的网络编程功能介绍。

接下来写个客户端的Demo,程序在客户端发送字符串到服务器,并将服务器端的反馈显示到控制台,数据交换只进行一次,当数据交换进行完成以后关闭网络连接,程序结束。

先来客户端的代码

 
  1. import java.io.InputStream; 
  2. import java.io.OutputStream; 
  3. import java.net.Socket; 
  4.  
  5. public class Client { 
  6.     public static void main(String[] args) { 
  7.         Socket socket = null
  8.         InputStream is = null
  9.         OutputStream os = null
  10.         try { 
  11.  
  12.             String msg = "Hello"
  13.             String ip = "localhost"
  14.             int port = 9898
  15.  
  16.             // 建立连接 
  17.             socket = new Socket(ip, port); 
  18.             // 发送数据 
  19.             os = socket.getOutputStream(); 
  20.             os.write(msg.getBytes()); 
  21.  
  22.             // 接收数据 
  23.             is = socket.getInputStream(); 
  24.             byte b[]= new byte[1024]; 
  25.             int n =is.read(b); 
  26.             System.out.println(new String(b,0,n)); 
  27.  
  28.         } catch (Exception e) { 
  29.             // TODO: handle exception 
  30.             e.printStackTrace(); 
  31.         } finally { 
  32.             try { 
  33.                 //关闭连接和流 
  34.                 is.close(); 
  35.                 os.close(); 
  36.                 socket.close(); 
  37.             } catch (Exception e2) { 
  38.                 // TODO: handle exception 
  39.                 e2.printStackTrace(); 
  40.             } 
  41.         } 
  42.     } 

代码中建服务器端的代码:

 
  1. public class Server { 
  2.     public static void main(String[] args) { 
  3.         ServerSocket serverSocket=null
  4.         Socket socket=null
  5.         InputStream is =null
  6.         OutputStream os =null
  7.          
  8.         try { 
  9.              
  10.             serverSocket = new ServerSocket(9898); 
  11.             socket = serverSocket.accept(); 
  12.              
  13.             is = socket.getInputStream(); 
  14.             byte b[] = new byte[1024]; 
  15.             int n = is.read(b); 
  16.             System.out.println("客户端发送了:"+new String(b,0,n)); 
  17.              
  18.             os = socket.getOutputStream(); 
  19.             os.write("接收成功!".getBytes()); 
  20.              
  21.         } catch (Exception e) { 
  22.             // TODO: handle exception 
  23.             e.printStackTrace(); 
  24.         }finally
  25.             try { 
  26.                 is.close(); 
  27.                 os.close(); 
  28.                 socket.close(); 
  29.                 serverSocket.close(); 
  30.             } catch (Exception e2) { 
  31.                 // TODO: handle exception 
  32.             } 
  33.         } 
  34.     } 

先运行服务器端,然后运行客户端,服务器接收到数据将数据打印出来之后再返回数据到客户端,客户端打印出来

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

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

 
  1. // 该代码实现的功能是监听当前计算机的9898号端口,如果在执行该代码时, 
  2. // 10000号端口已经被别的程序占用,那么将抛出异常。否则将实现监听。 
  3. serverSocket = new ServerSocket(9898); 

  服务器端编程的第二个步骤是获得连接。该步骤的作用是当有客户端连接到达时,建立一个和客户端连接对应的Socket连 接对象,从而释放客户端连接对于服务器端端口的占用。    通过获得连接,使得客户端的连接在服务器端获得了保持,另外使得服务器端的端口释放出来,可以继续等待其它的客户端连接。 实现获得连接的代码是:

 
  1. socket = serverSocket.accept(); 

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

连接获得以后,后续的编程就和客户端的网络编程类似了,这里获得的Socket类型的连接就和客户端的网络连接一样了,只是服务器端需要首先读取发送过来的数据,然后进行逻辑处理以后再发送给客户端,也就是交换数据的顺序和客户端交换数据的步骤刚好相反。这部分的内容和客户端很类似。

--------------------------

  上面这个示例只是演示了网络编程的基本步骤以及各个功能方法的基本使用,只是为网络编程打下了一个基础,下面将就几个问题来深入介绍网络编程深层次的一些知识。

1.如何复用Socket连接?

拨通一次电话以后多次对话,这就是复用客户端连接。

建立连接以后,将数据交换的逻辑写到一个循环中,只要循环不结束则连接就不会被关闭,按照这种思路,可以改造一下上面的代码,让该程序可以在建立连接一次以后,发送三次数据,当然这里的次数也可以是多次

现在看下新的服务器代码和客户端代码:

 
  1. import java.io.InputStream; 
  2. import java.io.OutputStream; 
  3. import java.net.ServerSocket; 
  4. import java.net.Socket; 
  5.  
  6. /** 
  7.  * 服务器代码 
  8.  * */ 
  9. public class Server { 
  10.     public static void main(String[] args) { 
  11.         ServerSocket serverSocket = null
  12.         Socket socket = null
  13.         InputStream is = null
  14.         OutputStream os = null
  15.  
  16.         try { 
  17.             serverSocket = new ServerSocket(9898); 
  18.             socket = serverSocket.accept(); 
  19.  
  20.             is = socket.getInputStream(); 
  21.             os = socket.getOutputStream(); 
  22.  
  23.             byte b[] = new byte[1024]; 
  24.  
  25.             for (int i = 0; i < 3; i++) { 
  26.                 int n = is.read(b); 
  27.                 os.write(("客户端发送的内容:" + new String(b, 0, n)).getBytes()); 
  28.             } 
  29.  
  30.         } catch (Exception e) { 
  31.             // TODO: handle exception 
  32.             e.printStackTrace(); 
  33.         } finally { 
  34.             try { 
  35.                 is.close(); 
  36.                 os.close(); 
  37.                 socket.close(); 
  38.                 serverSocket.close(); 
  39.             } catch (Exception e2) { 
  40.                 // TODO: handle exception 
  41.             } 
  42.         } 
  43.     } 

 再看下新的客户端代码:

 
  1. import java.io.InputStream; 
  2. import java.io.OutputStream; 
  3. import java.net.Socket; 
  4.  
  5. /** 
  6.  * 客户端代码 
  7.  * */ 
  8. public class Client { 
  9.     public static void main(String[] args) { 
  10.         Socket socket = null
  11.         InputStream is = null
  12.         OutputStream os = null
  13.         try { 
  14.  
  15.             String msg[] = { "one""two""three" }; 
  16.             String ip = "localhost"
  17.             int port = 9898
  18.  
  19.             // 建立连接 
  20.             socket = new Socket(ip, port); 
  21.             // 发送数据 
  22.             os = socket.getOutputStream(); 
  23.             // 接收数据 
  24.             is = socket.getInputStream(); 
  25.  
  26.             byte b[] = new byte[1024]; 
  27.  
  28.             for (int i = 0; i < msg.length; i++) { 
  29.                 os.write(msg[i].getBytes()); 
  30.                 int n = is.read(b); 
  31.                 System.out.println(new String(b, 0, n)); 
  32.             } 
  33.  
  34.         } catch (Exception e) { 
  35.             // TODO: handle exception 
  36.             e.printStackTrace(); 
  37.         } finally { 
  38.             try { 
  39.                 // 关闭连接和流 
  40.                 is.close(); 
  41.                 os.close(); 
  42.                 socket.close(); 
  43.             } catch (Exception e2) { 
  44.                 // TODO: handle exception 
  45.                 e2.printStackTrace(); 
  46.             } 
  47.         } 
  48.     } 

 上面的代码虽然比较简单,但是通用性比较差。

 

在该程序中,比较明显的体现出了“请求-响应”模型,也就是在客户端发起连接以后,首先发送字符串“First”给服务器端,服务器端输出客户端发送的内容“First”,然后将客户端发送的内容再反馈给客户端,这样客户端也输出服务器反馈“First”,这样就完成了客户端和服务器端的一次对话

三次会话的过程一样,在这个过程中,每次都是客户端程序首先发送数据给服务器端,服务器接收数据以后,将结果反馈给客户端,客户端接收到服务器端的反馈,从而完成一次通讯过程。

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

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

改造之后的服务器代码,可以接收多个客户端的数据。

 在该示例代码中,实现了一个while形式的死循环,由于accept方法是阻塞方法,所以当客户端连接未到达时,将阻塞该程序的执行,当客户端到达时接收该连接,并启动一个新的LogicThread线程处理该连接,然后按照循环的执行流程,继续等待下一个客户端连接。这样当任何一个客户端连接到达时,都开启一个专门的线程处理,通过多个线程支持多个客户端同时处理。
 
  1. /** 
  2.  * 支持多客户端的服务器代码 
  3.  * */ 
  4.  
  5. public class Server { 
  6.     public static void main(String[] args) { 
  7.         ServerSocket serverSocket = null
  8.         Socket socket = null
  9.  
  10.         try { 
  11.             serverSocket = new ServerSocket(9898); 
  12.  
  13.             while (true) { 
  14.                 socket = serverSocket.accept(); 
  15.                 // 启动线程 
  16.                 // 实现接收客户端连接,然后开启专门的逻辑线程处理该连接, 
  17.                 // LogicThread类实现对于一个客户端连接的逻辑处理,将处理的逻辑放置在该类的run方法中。 
  18.                 new LogicThread(socket); 
  19.             } 
  20.  
  21.         } catch (Exception e) { 
  22.             // TODO: handle exception 
  23.             e.printStackTrace(); 
  24.         } finally { 
  25.             try { 
  26.                 socket.close(); 
  27.                 serverSocket.close(); 
  28.             } catch (Exception e2) { 
  29.                 // TODO: handle exception 
  30.             } 
  31.         } 
  32.     } 
  33.  
  34.     static class LogicThread extends Thread { 
  35.  
  36.         Socket socket = null
  37.  
  38.         public LogicThread(Socket socket) { 
  39.             this.socket = socket; 
  40.             start(); 
  41.         } 
  42.  
  43.         @Override 
  44.         public void run() { 
  45.             byte b[] = new byte[1024]; 
  46.  
  47.             InputStream is = null
  48.             OutputStream os = null
  49.  
  50.             try { 
  51.                 is = socket.getInputStream(); 
  52.                 os = socket.getOutputStream(); 
  53.                 int n = is.read(b); 
  54.                 os.write(("客户端发送的内容:" + new String(b, 0, n)).getBytes()); 
  55.             } catch (Exception e) { 
  56.                 // TODO: handle exception 
  57.                 e.printStackTrace(); 
  58.  
  59.             } finally { 
  60.                 try { 
  61.                     is.close(); 
  62.                     os.close(); 
  63.                     socket.close(); 
  64.                 } catch (Exception e2) { 
  65.                     // TODO: handle exception 
  66.                 } 
  67.             } 
  68.  
  69.         } 
  70.     } 

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

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

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