转自:http://www.cnblogs.com/springcsc/archive/2009/12/03/1616413.html 多谢分享
网络编程
网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编程的大门而放弃了对于该部分技术的学习。
在学习网络编程以前,很多初学者可能觉得网络编程是比较复杂的系统工程,需要了解很多和网络相关的基础知识,其实这些都不是很必需的。首先来问一个问题:你 会打手机吗?很多人可能说肯定会啊,不就是按按电话号码,拨打电话嘛,很简单的事情啊!其实初学者如果入门网络编程的话也可以做到这么简单!
网络编程就是在两个或两个以上的设备(例如计算机)之间传输数据。程序员所作的事情就是把数据发送到指定的位置,或者接收到指定的数据,这个就是狭义的网络编程范畴。在发送和接收数据时,大部分的程序设计语言都设计了专门的API实现这些功能,程序员只需要调用即可。所以,基础的网络编程可以和打电话一样简单。
13.1 网络概述
网络编程技术是当前一种主流的编程技术,随着联网趋势的逐步增强以及网络应用程序的大量出现,所以在实际的开发中网络编程技术获得了大量的使用。本章中以浅 显的基础知识说明和实际的案例使广大初学者能够进入网络编程技术的大门,至于以后的实际修行就要阅读进阶的书籍以及进行大量的实际练习。
13.1.1 计算机网络概述
网络编程的实质就是两个(或多个)设备(例如计算机)之间的数据传输,按照计算机网络的定义,通过一定的物理设备将处于不同位置的计算机连接起来组成的网络,这个网络中包含的设备有:计算机、路由器、交换机等等,其实从软件编程的角度来说,对于物理设备的理解不需要很深刻,就像你打电话时不需要很熟悉通信网络的底层实现是一样的,但是当深入到网络编程的底层时,这些基础知识是必须要补的。
路由器和交换机组成了核心的计算机网络,计算机只是这个网络上的节点以及控制等,通过光纤、网线等连接将设备连接起来,从而形成了一张巨大的计算机网络,网络最主要的优势在于共享:共享设备和数据,现在共享设备最常见的是打印机,一个公司一般一个打印机即可,共享数据就是将大量的数据存储在一组机器中,其它的计算机通过网络访问这些数据,例如网站、银行服务器等等,如果需要了解更多的网络硬件基础知识,可以阅读《计算机网络》教材,对于基础进行强化,这个在基础学习阶段不是必须的,但是如果想在网络编程领域有所造诣,则是一个必须的基本功。
对于网络编程来说,最主要的是计算机和计算机之间的通信,这样首要的问题就是如何找到网络上的计算机呢?这就需要了解IP地址的概念。为了能够方便的识别网络上的每个设备,网络中的每个设备都会有一个唯一的数字标识,这个就是IP地址。在计算机网络中,现在命名IP地址的规定是IPv4协议,该协议规定每个IP地址由4个0-255之间的数字组成,例如10.0.120.34。每个接入网络的计算机都拥有唯一的IP地址,这个IP地址可能是固定的,例如网络上各种各样的服务器,也可以是动态的,例如使用ADSL拨号上网的宽带用户,无论以何种方式获得或是否是固定的,每个计算机在联网以后都拥有一个唯一的合法IP地址,就像每个手机号码一样。
但是由于IP地址不容易记忆,所以为了方便记忆,有创造了另外一个概念——域名(Domain Name),例如sohu.com等。一个IP地址可以对应多个域名,一个域名只能对应一个IP地址。域名的概念可以类比手机中的通讯簿,由于手机号码不方便记忆,所以添加一个姓名标识号码,在实际拨打电话时可以选择该姓名,然后拨打即可。
在网络中传输的数据,全部是以IP地址作为地址标识,所以在实际传输数据以前需要将域名转换为IP地址,实现这种功能的服务器称之为DNS服务器,也就是通俗的说法叫做域名解析。例如当用户在浏览器输入域名时,浏览器首先请求DNS服务器,将域名转换为IP地址,然后将转换后的IP地址反馈给浏览器,然后再进行实际的数据传输。当DNS服务器正常工作时,使用IP地址或域名都可以很方便的找到计算机网络中的某个设备,例如服务器计算机。当DNS不正常工作时,只能通过IP地址访问该设备。所以IP地址的使用要比域名通用一些。
IP地址和域名很好的解决了在网络中找到一个计算机的问题,但是为了让一个计算机可以同时运行多个网络程序,就引入了另外一个概念——端口(port)。在介绍端口的概念以前,首先来看一个例子,一般一个公司前台会有一个电话,每个员工会有一个分机,这样如果需要找到这个员工的话,需要首先拨打前台总机,然后转该分机号即可。这样减少了公司的开销,也方便了每个员工。在该示例中前台总机的电话号码就相当于IP地址,而每个员工的分机号就相当于端口,有了端口的概念以后,在同一个计算机中每个程序对应唯一的端口,这样一个计算机上就可以通过端口区分发送给每个端口的数据了,换句话说,也就是一个计算机上可以并发运行多个网络程序,而不会在互相之间产生干扰。在硬件上规定,端口的号码必须位于0-65535之间,每个端口唯一的对应一个网络程序,一个网络程序可以使用多个端口。这样一个网络程序运行在一台计算上时,不管是客户端还是服务器,都是至少占用一个端口进行网络通讯。在接收数据时,首先发送给对应的计算机,然后计算机根据端口把数据转发给对应的程序。
有了IP地址和端口的概念以后,在进行网络通讯交换时,就可以通过IP地址查找到该台计算机,然后通过端口标识这台计算机上的一个唯一的程序。这样就可以进行网络数据的交换了。但是,进行网络编程时,只有IP地址和端口的概念还是不够的,下面就介绍一下基础的网络编程相关的软件基础知识。
13.1. 2 网络编程概述
按照前面的介绍,网络编程就是两个或多个设备之间的数据交换,其实更具体的说,网络编程就是两个或多个程序之间的数据交换,和普通的单机程序相比,网络程序最大的不同就是需要交换数据的程序运行在不同的计算机上,这样就造成了数据交换的复杂。虽然通过IP地址和端口可以找到网络上运行的一个程序,但是如果需要进行网络编程,则还需要了解网络通讯的过程。
网络通讯基于“请求-响应”模型。为了理解这个模型,先来看一个例子,经常看电视的人肯定见过审讯的场面吧,一般是这样的:
警察:姓名
嫌疑犯:XXX
警察:性别
嫌疑犯:男
警察:年龄
嫌疑犯:29
……
在这个例子中,警察问一句,嫌疑犯回答一句,如果警察不问,则嫌疑犯保持沉默。这种一问一答的形式就是网络中的“请求-响应”模型。也就是通讯的一端发送数据,另外一端反馈数据,网络通讯都基于该模型。
在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。由此,网络编程中的两种程序就分别是客户端和服务器端,例如QQ程序,每个QQ用户安装的都是QQ客户端程序,而QQ服务器端程序则运行在腾讯公司的机房中,为大量的QQ用户提供服务。这种网络编程的结构被称作客户端/服务器结构,也叫做Client/Server结构,简称C/S结构。使用C/S结 构的程序,在开发时需要分别开发客户端和服务器端,这种结构的优势在于由于客户端是专门开发的,所以根据需要实现各种效果,专业点说就是表现力丰富,而服 务器端也需要专门进行开发。但是这种结构也存在着很多不足,例如通用性差,几乎不能通用等,也就是说一种程序的客户端只能和对应的服务器端通讯,而不能和 其它服务器端通讯,在实际维护时,也需要维护专门的客户端和服务器端,维护的压力比较大。其实在运行很多程序时,没有必要使用专用的客户端,而需要使用通用的客户端,例如浏览器,使用浏览器作为客户端的结构被称作浏览器/服务器结构,也叫做Browser/Server结构,简称为B/S结构。
使用B/S结构的程序,在开发时只需要开发服务器端即可,这种结构的优势在于开发的压力比较小,不需要维护客户端。但是这种结构也存在着很多不足,例如浏览器的限制比较大,表现力不强,无法进行系统级操作等。总之C/S结构和B/S结构是现在网络编程中常见的两种结构,B/S结构其实也就是一种特殊的C/S结构。
另外简单的介绍一下P2P(Point to Point)程序,常见的如BT、电驴等。P2P程序是一种特殊的程序,应该一个P2P程序中既包含客户端程序,也包含服务器端程序,例如BT,使用客户端程序部分连接其它的种子(服务器端),而使用服务器端向其它的BT客户端传输数据。如果这个还不是很清楚,其实P2P程序和手机是一样的,当手机拨打电话时就是使用客户端的作用,而手机处于待机状态时,可以接收到其它用户拨打的电话则起的就是服务器端的功能,只是一般的手机不能同时使用拨打电话和接听电话的功能,而P2P程序实现了该功能。
最后再介绍一个网络编程中最重要,也是最复杂的概念——协议(Protocol)。按照前面的介绍,网络编程就是运行在不同计算机中两个程序之间的数据交换。在实际进行数据交换时,为了让接收端理解该数据,计算机比较笨,什么都不懂的,那么就需要规定该数据的格式,这个数据的格式就是协议。
如果没有理解协议的概念,那么再举一个例子,记得有个电影叫《永不消逝的电波》,讲述的是地下党通过电台发送情报的故事,这里我们不探讨电影的剧情,而只关 心电台发送的数据。在实际发报时,需要首先将需要发送的内容转换为电报编码,然后将电报编码发送出去,而接收端接收的是电报编码,如果需要理解电报的内容 则需要根据密码本翻译出该电报的内容。这里的密码本就规定了一种数据格式,这种对于网络中传输的数据格式在网络编程中就被称作协议。
那么如何来编写协议格式呢?答案是随意。只要按照这种协议格式能够生成唯一的编码,按照该编码可以唯一的解析出发送数据的内容即可。也正因为各个网络程序之间协议格式的不同,所以才导致了客户端程序都是专用的结构。
在实际的网络程序编程中,最麻烦的内容不是数据的发送和接收,因为这个功能在几乎所有的程序语言中都提供了封装好的API进行调用,最麻烦的内容就是协议的设计以及协议的生产和解析,这个才是网络编程中最核心的内容。
关于网络编程的基础知识,就介绍这里,深刻理解IP地址、端口和协议等概念,将会极大的有助于后续知识的学习。
13.1.3 网络通讯方式
在现有的网络中,网络通讯的方式主要有两种:
1、 TCP(传输控制协议)方式
2、 UDP(用户数据报协议)方式
为了方便理解这两种方式,还是先来看一个例子。大家使用手机时,向别人传递信息时有两种方式:拨打电话和发送短信。使用拨打电话的方式可以保证将信息传递给 别人,因为别人接听电话时本身就确认接收到了该信息。而发送短信的方式价格低廉,使用方便,但是接收人有可能接收不到。
在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。
这两种传输方式都是实际的网络编程中进行使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则都通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据的传递。
由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。
13.2 网络编程技术
前面介绍了网络编程的相关基础知识,初步建立了网络编程的概念,但是实际学习网络编程还必须使用某种程序设计语言进行代码实现,下面就介绍一下网络编程的代码实现。
13.2.1 网络编程步骤
按照前面的基础知识介绍,无论使用TCP方式还是UDP方式进行网络通讯,网络编程都是由客户端和服务器端组成。当然,B/S结构的编程中只需要实现服务器端即可。所以,下面介绍网络编程的步骤时,均以C/S结构为基础进行介绍。说明:这里的步骤实现和语言无关,也就是说,这个步骤适用于各种语言实现,不局限于Java语言。
13.2.1.1 客户端网络编程步骤
客户端(Client)是指网络编程中首先发起连接的程序,客户端一般实现程序界面和基本逻辑实现,在进行实际的客户端编程时,无论客户端复杂还是简单,以及客户端实现的方式,客户端的编程主要由三个步骤实现:
1、 建立网络连接
客户端网络编程的第一步都是建立网络连接,在建立网络连接时需要指定连接到的服务器的IP地址和端口号,建立完成以后,会形成一条虚拟的连接,后续的操作就可以通过该连接实现数据交换了。
2、 交换数据
连接建立以后,就可以通过这个连接交换数据了,交换数据严格按照请求响应模型进行,由客户端发送一个请求数据到服务器,服务器反馈一个响应数据给客户端,如果客户端不发送请求则服务器端就不响应。根据逻辑需要,可以多次交换数据,但是还是必须遵循请求响应模型。
3、 关闭网络连接
在数据交换完成以后,关闭网络连接,释放程序占用的端口、内存等系统资源,结束网络编程。
最基本的步骤一般都是这三个步骤,在实际实现时,步骤2会出现重复,在进行代码组织时,由于网络编程是比较耗时的操作,所以一般开启专门的现场进行网络通讯。
13.2.1.2 服务器端网络编程步骤
服务器端(Server)是指在网络编程中被动等待连接的程序,服务器端一般实现程序的核心逻辑以及数据存储等核心功能。服务器端的编程步骤和客户端不同,是由四个步骤实现,依次是:
1、 监听端口
服务器端属于被动等待连接,所以服务器端启动以后,不需要发起连接,而只需要监听本地计算机的某个固定端口即可。这个端口就是服务器端开放给客户端的端口,服务器端程序运行的本地计算机的IP地址就是服务器端程序的IP地址。
2、 获得连接
当客户端连接到服务器端时,服务器端就可以获得一个连接,这个连接包含客户端的信息,例如客户端IP地址等等,服务器端和客户端也通过该连接进行数据交换。一般在服务器端编程中,当获得连接时,需要开启专门的线程处理该连接,每个连接都由独立的线程实现。
3、 交换数据
服务器端通过获得的连接进行数据交换。服务器端的数据交换步骤是首先接收客户端发送过来的数据,然后进行逻辑处理,再把处理以后的结果数据发送给客户端。简单来说,就是先接收再发送,这个和客户端的数据交换数序不同。其实,服务器端获得的连接和客户端连接是一样的,只是数据交换的步骤不同。当然,服务器端的数据交换也是可以多次进行的。在数据交换完成以后,关闭和客户端的连接。
4、 关闭连接
当服务器程序关闭时,需要关闭服务器端,通过关闭服务器端使得服务器监听的端口以及占用的内存可以释放出来,实现了连接的关闭。其实服务器端编程的模型和呼叫中心的实现是类似的,例如移动的客服电话10086就是典型的呼叫中心,当一个用户拨打10086时,转接给一个专门的客服人员,由该客服实现和该用户的问题解决,当另外一个用户拨打10086时,则转接给另一个客服,实现问题解决,依次类推。
在服务器端编程时,10086这个电话号码就类似于服务器端的端口号码,每个用户就相当于一个客户端程序,每个客服人员就相当于服务器端启动的专门和客户端连接的线程,每个线程都是独立进行交互的。
这就是服务器端编程的模型,只是TCP方式是需要建立连接的,对于服务器端的压力比较大,而UDP是不需要建立连接的,对于服务器端的压力比较小罢了。
13.2.1.3 小结
总之,无论使用任何语言,任何方式进行基础的网络编程,都必须遵循固定的步骤进行操作,在熟悉了这些步骤以后,可以根据需要进行逻辑上的处理,但是还是必须遵循固定的步骤进行。
其实,基础的网络编程本身不难,也不需要很多的基础网络知识,只是由于编程的基础功能都已经由API实现,而且需要按照固定的步骤进行,所以在入门时有一定的门槛,希望下面的内容能够将你快速的带入网络编程技术的大门。
13.2.2 Java网络编程技术
Java语言是在网络环境下诞生的,所以Java语言虽然不能说是对于网络编程的支持最好的语言,但是必须说是一种对于网络编程提供良好支持的语言,使用Java语言进行网络编程将是一件比较轻松的工作。
和网络编程有关的基本API位于java.net包中,该包中包含了基本的网络编程实现,该包是网络编程的基础。该包中既包含基础的网络编程类,也包含封装后的专门处理WEB相关的处理类。在本章中,将只介绍基础的网络编程类。
首先来介绍一个基础的网络类——InetAddress类。该类的功能是代表一个IP地址,并且将IP地址和域名相关的操作方法包含在该类的内部。
关于该类的使用,下面通过一个基础的代码示例演示该类的使用,代码如下:
1 package inetaddressdemo; 2 3 import java.net.*; 4 5 /** 6 7 * 演示InetAddress类的基本使用 8 9 */ 10 11 public class InetAddressDemo { 12 13 public static void main(String[] args) { 14 15 try{ 16 17 //使用域名创建对象 18 19 InetAddress inet1 = InetAddress.getByName("www.163.com"); 20 21 System.out.println(inet1); 22 23 //使用IP创建对象 24 25 InetAddress inet2 = InetAddress.getByName("127.0.0.1"); 26 27 System.out.println(inet2); 28 29 //获得本机地址对象 30 31 InetAddress inet3 = InetAddress.getLocalHost(); 32 33 System.out.println(inet3); 34 35 //获得对象中存储的域名 36 37 String host = inet3.getHostName(); 38 39 System.out.println("域名:" + host); 40 41 //获得对象中存储的IP 42 43 String ip = inet3.getHostAddress(); 44 45 System.out.println("IP:" + ip); 46 47 }catch(Exception e){} 48 49 } 50 51 }
在该示例代码中,演示了InetAddress类的基本使用,并使用了该类中的几个常用方法,该代码的执行结果是:
www.163.com/220.181.28.50
/127.0.0.1
chen/192.168.1.100
域名:chen
IP:192.168.1.100
说明:由于该代码中包含一个互联网的网址,所以运行该程序时需要联网,否则将产生异常。
在后续的使用中,经常包含需要使用InetAddress对象代表IP地址的构造方法,当然,该类的使用不是必须的,也可以使用字符串来代表IP地址进行实现。
13.2.3 TCP编程
按照前面的介绍,网络通讯的方式有TCP和UDP两种,其中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”,并将服务器端的反馈显示到控制台,数据交换只进行一次,当数据交换进行完成以后关闭网络连接,程序结束。实现的代码如下:
1 package tcp; 2 3 import java.io.*; 4 5 import java.net.*; 6 7 /** 8 9 * 简单的Socket客户端 10 11 * 功能为:发送字符串“Hello”到服务器端,并打印出服务器端的反馈 12 13 */ 14 15 public class SimpleSocketClient { 16 17 public static void main(String[] args) { 18 19 Socket socket = null; 20 21 InputStream is = null; 22 23 OutputStream os = null; 24 25 //服务器端IP地址 26 27 String serverIP = "127.0.0.1"; 28 29 //服务器端端口号 30 31 int port = 10000; 32 33 //发送内容 34 35 String data = "Hello"; 36 37 try { 38 39 //建立连接 40 41 socket = new Socket(serverIP,port); 42 43 //发送数据 44 45 os = socket.getOutputStream(); 46 47 os.write(data.getBytes()); 48 49 //接收数据 50 51 is = socket.getInputStream(); 52 53 byte[] b = new byte[1024]; 54 55 int n = is.read(b); 56 57 //输出反馈数据 58 59 System.out.println("服务器反馈:" + new String(b,0,n)); 60 61 } catch (Exception e) { 62 63 e.printStackTrace(); //打印异常信息 64 65 }finally{ 66 67 try { 68 69 //关闭流和连接 70 71 is.close(); 72 73 os.close(); 74 75 socket.close(); 76 77 } catch (Exception e2) {} 78 79 } 80 81 } 82 83 }
在该示例代码中建立了一个连接到IP地址为127.0.0.1,端口号码为10000的TCP类型的网络连接,然后获得连接的输出流对象,将需要发送的字符串“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服务器端实现的功能就是将客户端发送的内容再原封不动的反馈给客户端。实现的代码如下:
1 package tcp; 2 3 import java.io.*; 4 5 import java.net.*; 6 7 /** 8 9 * echo服务器 10 11 * 功能:将客户端发送的内容反馈给客户端 12 13 */ 14 15 public class SimpleSocketServer { 16 17 public static void main(String[] args) { 18 19 ServerSocket serverSocket = null; 20 21 Socket socket = null; 22 23 OutputStream os = null; 24 25 InputStream is = null; 26 27 //监听端口号 28 29 int port = 10000; 30 31 try { 32 33 //建立连接 34 35 serverSocket = new ServerSocket(port); 36 37 //获得连接 38 39 socket = serverSocket.accept(); 40 41 //接收客户端发送内容 42 43 is = socket.getInputStream(); 44 45 byte[] b = new byte[1024]; 46 47 int n = is.read(b); 48 49 //输出 50 51 System.out.println("客户端发送内容为:" + new String(b,0,n)); 52 53 //向客户端发送反馈内容 54 55 os = socket.getOutputStream(); 56 57 os.write(b, 0, n); 58 59 } catch (Exception e) { 60 61 e.printStackTrace(); 62 63 }finally{ 64 65 try{ 66 67 //关闭流和连接 68 69 os.close(); 70 71 is.close(); 72 73 socket.close(); 74 75 serverSocket.close(); 76 77 }catch(Exception e){} 78 79 } 80 81 } 82 83 }
在该示例代码中建立了一个监听当前计算机10000号端口的服务器端Socket连接,然后获得客户端发送过来的连接,如果有连接到达时,读取连接中发送过来的内容,并将发送的内容在控制台进行输出,输出完成以后将客户端发送的内容再反馈给客户端。最后关闭流和连接对象,结束程序。
在控制台下面编译和运行该程序的命令和客户端部分的类似。
这样,就以一个很简单的示例演示了TCP类型的网络编程在Java语言中的基本实现,这个示例只是演示了网络编程的基本步骤以及各个功能方法的基本使用,只是为网络编程打下了一个基础,下面将就几个问题来深入介绍网络编程深层次的一些知识。
为了一步一步的掌握网络编程,下面再研究网络编程中的两个基本问题,通过解决这两个问题将对网络编程的认识深入一层。
1、如何复用Socket连接?
在前面的示例中,客户端中建立了一次连接,只发送一次数据就关闭了,这就相当于拨打电话时,电话打通了只对话一次就关闭了,其实更加常用的应该是拨通一次电话以后多次对话,这就是复用客户端连接,那么如何实现建立一次连接,进行多次数据交换呢?其实很简单,建立连接以后,将数据交换的逻辑写到一个循环中就可以了。这样只要循环不结束则连接就不会被关闭。按照这种思路,可以改造一下上面的代码,让该程序可以在建立连接一次以后,发送三次数据,当然这里的次数也可以是多次,示例代码如下:
1 package tcp; 2 3 import java.io.*; 4 5 import java.net.*; 6 7 /** 8 9 * 复用连接的Socket客户端 10 11 * 功能为:发送字符串“Hello”到服务器端,并打印出服务器端的反馈 12 13 */ 14 15 public class MulSocketClient { 16 17 public static void main(String[] args) { 18 19 Socket socket = null; 20 21 InputStream is = null; 22 23 OutputStream os = null; 24 25 //服务器端IP地址 26 27 String serverIP = "127.0.0.1"; 28 29 //服务器端端口号 30 31 int port = 10000; 32 33 //发送内容 34 35 String data[] ={"First","Second","Third"}; 36 37 try { 38 39 //建立连接 40 41 socket = new Socket(serverIP,port); 42 43 //初始化流 44 45 os = socket.getOutputStream(); 46 47 is = socket.getInputStream(); 48 49 byte[] b = new byte[1024]; 50 51 for(int i = 0;i < data.length;i++){ 52 53 //发送数据 54 55 os.write(data[i].getBytes()); 56 57 //接收数据 58 59 int n = is.read(b); 60 61 //输出反馈数据 62 63 System.out.println("服务器反馈:" + new String(b,0,n)); 64 65 } 66 67 } catch (Exception e) { 68 69 e.printStackTrace(); //打印异常信息 70 71 }finally{ 72 73 try { 74 75 //关闭流和连接 76 77 is.close(); 78 79 os.close(); 80 81 socket.close(); 82 83 } catch (Exception e2) {} 84 85 } 86 87 } 88 89 }
该示例程序和前面的代码相比,将数据交换部分的逻辑写在一个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
显然,客户端在实际运行时出现了异常,出现异常的原因是什么呢?如果仔细阅读前面的代码,应该还记得前面示例代码中的服务器端是对话一次数据以后就关闭了连接,如果服务器端程序关闭了,客户端继续发送数据肯定会出现异常,这就是出现该问题的原因。按照客户端实现的逻辑,也可以复用服务器端的连接,实现的原理也是将服务器端的数据交换逻辑写在循环中即可,按照该种思路改造以后的服务器端代码为:
1 package tcp; 2 3 import java.io.*; 4 5 import java.net.*; 6 7 /** 8 9 * 复用连接的echo服务器 10 11 * 功能:将客户端发送的内容反馈给客户端 12 13 */ 14 15 public class MulSocketServer { 16 17 public static void main(String[] args) { 18 19 ServerSocket serverSocket = null; 20 21 Socket socket = null; 22 23 OutputStream os = null; 24 25 InputStream is = null; 26 27 //监听端口号 28 29 int port = 10000; 30 31 try { 32 33 //建立连接 34 35 serverSocket = new ServerSocket(port); 36 37 System.out.println("服务器已启动:"); 38 39 //获得连接 40 41 socket = serverSocket.accept(); 42 43 //初始化流 44 45 is = socket.getInputStream(); 46 47 os = socket.getOutputStream(); 48 49 byte[] b = new byte[1024]; 50 51 for(int i = 0;i < 3;i++){ 52 53 int n = is.read(b); 54 55 //输出 56 57 System.out.println("客户端发送内容为:" + new String(b,0,n)); 58 59 //向客户端发送反馈内容 60 61 os.write(b, 0, n); 62 63 } 64 65 } catch (Exception e) { 66 67 e.printStackTrace(); 68 69 }finally{ 70 71 try{ 72 73 //关闭流和连接 74 75 os.close(); 76 77 is.close(); 78 79 socket.close(); 80 81 serverSocket.close(); 82 83 }catch(Exception e){} 84 85 } 86 87 } 88 89 }
在该示例代码中,也将数据发送和接收的逻辑写在了一个for循环内部,只是在实现时硬性的将循环次数规定成了3次,这样代码虽然比较简单,但是通用性比较差。以该服务器端代码实现为基础运行前面的客户端程序时,客户端的输出为:
服务器反馈:First
服务器反馈:Second
服务器反馈:Third
服务器端程序的输出结果为:
服务器已启动:
客户端发送内容为:First
客户端发送内容为:Second
客户端发送内容为:Third
在该程序中,比较明显的体现出了“请求-响应”模型,也就是在客户端发起连接以后,首先发送字符串“First”给服务器端,服务器端输出客户端发送的内容“First”,然后将客户端发送的内容再反馈给客户端,这样客户端也输出服务器反馈“First”,这样就完成了客户端和服务器端的一次对话,紧接着客户端发送“Second”给服务器端,服务端输出“Second”,然后将“Second”再反馈给客户端,客户端再输出“Second”,从而完成第二次会话,第三次会话的过程和这个一样。在这个过程中,每次都是客户端程序首先发送数据给服务器端,服务器接收数据以后,将结果反馈给客户端,客户端接收到服务器端的反馈,从而完成一次通讯过程。
在该示例中,虽然解决了多次发送的问题,但是客户端和服务器端的次数控制还不够灵活,如果客户端的次数不固定怎么办呢?是否可以使用某个特殊的字符串,例如quit,表示客户端退出呢,这就涉及到网络协议的内容了,会在后续的网络应用示例部分详细介绍。下面开始介绍另外一个网络编程的突出问题。
2、如何使服务器端支持多个客户端同时工作?
前面介绍的服务器端程序,只是实现了概念上的服务器端,离实际的服务器端程序结构距离还很遥远,如果需要让服务器端能够实际使用,那么最需要解决的问题就是——如何支持多个客户端同时工作。
一个服务器端一般都需要同时为多个客户端提供通讯,如果需要同时支持多个客户端,则必须使用前面介绍的线程的概念。简单来说,也就是当服务器端接收到一个连接时,启动一个专门的线程处理和该客户端的通讯。
按照这个思路改写的服务端示例程序将由两个部分组成,MulThreadSocketServer类实现服务器端控制,实现接收客户端连接,然后开启专门的逻辑线程处理该连接,LogicThread类实现对于一个客户端连接的逻辑处理,将处理的逻辑放置在该类的run方法中。该示例的代码实现为:
1 package tcp; 2 3 import java.net.ServerSocket; 4 5 import java.net.Socket; 6 7 /** 8 9 * 支持多客户端的服务器端实现 10 11 */ 12 13 public class MulThreadSocketServer { 14 15 public static void main(String[] args) { 16 17 ServerSocket serverSocket = null; 18 19 Socket socket = null; 20 21 //监听端口号 22 23 int port = 10000; 24 25 try { 26 27 //建立连接 28 29 serverSocket = new ServerSocket(port); 30 31 System.out.println("服务器已启动:"); 32 33 while(true){ 34 35 //获得连接 36 37 socket = serverSocket.accept(); 38 39 //启动线程 40 41 new LogicThread(socket); 42 43 } 44 45 } catch (Exception e) { 46 47 e.printStackTrace(); 48 49 }finally{ 50 51 try{ 52 53 //关闭连接 54 55 serverSocket.close(); 56 57 }catch(Exception e){} 58 59 } 60 61 } 62 63 }
在该示例代码中,实现了一个while形式的死循环,由于accept方法是阻塞方法,所以当客户端连接未到达时,将阻塞该程序的执行,当客户端到达时接收该连接,并启动一个新的LogicThread线程处理该连接,然后按照循环的执行流程,继续等待下一个客户端连接。这样当任何一个客户端连接到达时,都开启一个专门的线程处理,通过多个线程支持多个客户端同时处理。
下面再看一下LogicThread线程类的源代码实现:
1 package tcp; 2 3 import java.io.*; 4 5 import java.net.*; 6 7 /** 8 9 * 服务器端逻辑线程 10 11 */ 12 13 public class LogicThread extends Thread { 14 15 Socket socket; 16 17 InputStream is; 18 19 OutputStream os; 20 21 public LogicThread(Socket socket){ 22 23 this.socket = socket; 24 25 start(); //启动线程 26 27 } 28 29 30 31 public void run(){ 32 33 byte[] b = new byte[1024]; 34 35 try{ 36 37 //初始化流 38 39 os = socket.getOutputStream(); 40 41 is = socket.getInputStream(); 42 43 for(int i = 0;i < 3;i++){ 44 45 //读取数据 46 47 int n = is.read(b); 48 49 //逻辑处理 50 51 byte[] response = logic(b,0,n); 52 53 //反馈数据 54 55 os.write(response); 56 57 } 58 59 }catch(Exception e){ 60 61 e.printStackTrace(); 62 63 }finally{ 64 65 close(); 66 67 } 68 69 } 70 71 72 73 /** 74 75 * 关闭流和连接 76 77 */ 78 79 private void close(){ 80 81 try{ 82 83 //关闭流和连接 84 85 os.close(); 86 87 is.close(); 88 89 socket.close(); 90 91 }catch(Exception e){} 92 93 } 94 95 96 97 /** 98 99 * 逻辑处理方法,实现echo逻辑 100 101 * @param b 客户端发送数据缓冲区 102 103 * @param off 起始下标 104 105 * @param len 有效数据长度 106 107 * @return 108 109 */ 110 111 private byte[] logic(byte[] b,int off,int len){ 112 113 byte[] response = new byte[len]; 114 115 //将有效数据拷贝到数组response中 116 117 System.arraycopy(b, 0, response, 0, len); 118 119 return response; 120 121 } 122 123 }
在该示例代码中,每次使用一个连接对象构造该线程,该连接对象就是该线程需要处理的连接,在线程构造完成以后,该线程就被启动起来了,然后在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方式的编程,包含客户端网络编程和服务器端网络编程,主要由两个类实现,分别是:
l DatagramSocket
DatagramSocket类实现“网络连接”,包括客户端网络连接和服务器端网络连接。虽然UDP方式的网络通讯不需要建立专用的网络连接,但是毕竟还是需要发送和接收数据,DatagramSocket实现的就是发送数据时的发射器,以及接收数据时的监听器的角色。类比于TCP中的网络连接,该类既可以用于实现客户端连接,也可以用于实现服务器端连接。
l 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”发送到IP是127.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对象中。在准备完成以后,调用连接对象ds的send方法把DatagramPacket对象发送出去即可,按照UDP协议的约定,在进行数据传输时,系统只是尽全力传输数据,但是并不保证数据一定被正确传输,如果数据在传输过程中丢失,那就丢失了。
UDP方式在进行网络通讯时,也遵循“请求-响应”模型,在发送数据完成以后,就可以接收服务器端的反馈数据了。下面介绍一下UDP客户端编程中接收数据的实现。当数据发送出去以后,就可以接收服务器端的反馈信息了。接收数据在Java语言中的实现是这样的:首先构造一个数据缓冲数组,该数组用于存储接收的服务器端反馈数据,该数组的长度必须大于或等于服务器端反馈的实际有效数据的长度。然后以该缓冲数组为基础构造一个DatagramPacket数据包对象,最后调用连接对象的receive方法接收数据即可。接收到的服务器端反馈数据存储在DatagramPacket类型的对象内部。实现接收数据以及显示服务器端反馈内容的示例代码如下:
1 //构造缓冲数组 2 3 byte[] data = new byte[1024]; 4 5 //构造数据包对象 6 7 DatagramPacket received = new DatagramPacket(data,data.length); 8 9 //接收数据 10 11 ds.receive(receiveDp); 12 13 //输出数据内容 14 15 byte[] b = receiveDp.getData(); //获得缓冲数组 16 17 int len = receiveDp.getLength(); //获得有效数据长度 18 19 String s = new String(b,0,len); 20 21 System.out.println(s);
在该代码中,首先构造缓冲数组data,这里设置的长度1024是预估的接收到的数据长度,要求该长度必须大于或等于接收到的数据长度,然后以该缓冲数组为基础,构造数据包对象,使用连接对象ds的receive方法接收反馈数据,由于在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”。实现该功能的客户端代码如下所示:
1 package udp; 2 3 import java.net.*; 4 5 import java.util.*; 6 7 /** 8 9 * 简单的UDP客户端,实现向服务器端发生系统时间功能 10 11 */ 12 13 public class SimpleUDPClient { 14 15 public static void main(String[] args) { 16 17 DatagramSocket ds = null; //连接对象 18 19 DatagramPacket sendDp; //发送数据包对象 20 21 DatagramPacket receiveDp; //接收数据包对象 22 23 String serverHost = "127.0.0.1"; //服务器IP 24 25 int serverPort = 10010; //服务器端口号 26 27 try{ 28 29 //建立连接 30 31 ds = new DatagramSocket(); 32 33 //初始化发送数据 34 35 Date d = new Date(); //当前时间 36 37 String content = d.toString(); //转换为字符串 38 39 byte[] data = content.getBytes(); 40 41 //初始化发送包对象 42 43 InetAddress address = InetAddress.getByName(serverHost); 44 45 sendDp = new DatagramPacket(data,data.length,address,serverPort); 46 47 //发送 48 49 ds.send(sendDp); 50 51 52 53 //初始化接收数据 54 55 byte[] b = new byte[1024]; 56 57 receiveDp = new DatagramPacket(b,b.length); 58 59 //接收 60 61 ds.receive(receiveDp); 62 63 //读取反馈内容,并输出 64 65 byte[] response = receiveDp.getData(); 66 67 int len = receiveDp.getLength(); 68 69 String s = new String(response,0,len); 70 71 System.out.println("服务器端反馈为:" + s); 72 73 }catch(Exception e){ 74 75 e.printStackTrace(); 76 77 }finally{ 78 79 try{ 80 81 //关闭连接 82 83 ds.close(); 84 85 }catch(Exception e){} 86 87 } 88 89 } 90 91 }
在该示例代码中,首先建立UDP方式的网络连接,然后获得当前系统时间,这里获得的系统时间是客户端程序运行的本地计算机的时间,然后将时间字符串以及服务器端的IP和端口,构造成发送数据包对象,调用连接对象ds的send方法发送出去。在数据发送出去以后,构造接收数据的数据包对象,调用连接对象ds的receive方法接收服务器端的反馈,并输出在控制台。最后在finally语句块中关闭客户端网络连接。
和下面将要介绍的服务器端一起运行时,客户端程序的输出结果为:
服务器端反馈为:OK
下面是该示例程序的服务器端代码实现:
1 package udp; 2 3 import java.net.*; 4 5 /** 6 7 * 简单UDP服务器端,实现功能是输出客户端发送数据, 8 9 并反馈字符串“OK"给客户端 10 11 */ 12 13 public class SimpleUDPServer { 14 15 public static void main(String[] args) { 16 17 DatagramSocket ds = null; //连接对象 18 19 DatagramPacket sendDp; //发送数据包对象 20 21 DatagramPacket receiveDp; //接收数据包对象 22 23 final int PORT = 10010; //端口 24 25 try{ 26 27 //建立连接,监听端口 28 29 ds = new DatagramSocket(PORT); 30 31 System.out.println("服务器端已启动:"); 32 33 //初始化接收数据 34 35 byte[] b = new byte[1024]; 36 37 receiveDp = new DatagramPacket(b,b.length); 38 39 //接收 40 41 ds.receive(receiveDp); 42 43 //读取反馈内容,并输出 44 45 InetAddress clientIP = receiveDp.getAddress(); 46 47 int clientPort = receiveDp.getPort(); 48 49 byte[] data = receiveDp.getData(); 50 51 int len = receiveDp.getLength(); 52 53 System.out.println("客户端IP:" + clientIP.getHostAddress()); 54 55 System.out.println("客户端端口:" + clientPort); 56 57 System.out.println("客户端发送内容:" + new String(data,0,len)); 58 59 60 61 //发送反馈 62 63 String response = "OK"; 64 65 byte[] bData = response.getBytes(); 66 67 sendDp = new DatagramPacket(bData,bData.length,clientIP,clientPort); 68 69 //发送 70 71 ds.send(sendDp); 72 73 }catch(Exception e){ 74 75 e.printStackTrace(); 76 77 }finally{ 78 79 try{ 80 81 //关闭连接 82 83 ds.close(); 84 85 }catch(Exception e){} 86 87 } 88 89 } 90 91 }
在该服务器端实现中,首先监听10010号端口,和TCP方式的网络编程类似,服务器端的receive方法是阻塞方法,如果客户端不发送数据,则程序会在该方法处阻塞。当客户端发送数据到达服务器端时,则接收客户端发送过来的数据,然后将客户端发送的数据内容读取出来,并在服务器端程序中打印客户端的相关信息,从客户端发送过来的数据包中可以读取出客户端的IP以及客户端端口号,将反馈数据字符串“OK”发送给客户端,最后关闭服务器端连接,释放占用的系统资源,完成程序功能示例。
和前面TCP方式中的网络编程类似,这个示例也仅仅是网络编程的功能示例,也存在前面介绍的客户端无法进行多次数据交换,以及服务器端不支持多个客户端的问题,这两个问题也需要对于代码进行处理才可以很方便的进行解决。
在解决该问题以前,需要特别指出的是UDP方式的网络编程由于不建立虚拟的连接,所以在实际使用时和TCP方式存在很多的不同,最大的一个不同就是“无状态”。该特点指每次服务器端都收到信息,但是这些信息和连接无关,换句话说,也就是服务器端只是从信息是无法识别出是谁发送的,这样就要求发送信息时的内容需要多一些,这个在后续的示例中可以看到。
下面是实现客户端多次发送以及服务器端支持多个数据包同时处理的程序结构,实现的原理和TCP方式类似,在客户端将数据的发送和接收放入循环中,而服务器端则将接收到的每个数据包启动一个专门的线程进行处理。实现的代码如下:
1 package udp; 2 3 import java.net.*; 4 5 import java.util.*; 6 7 /** 8 9 * 简单的UDP客户端,实现向服务器端发生系统时间功能 10 11 * 该程序发送3次数据到服务器端 12 13 */ 14 15 public class MulUDPClient { 16 17 public static void main(String[] args) { 18 19 DatagramSocket ds = null; //连接对象 20 21 DatagramPacket sendDp; //发送数据包对象 22 23 DatagramPacket receiveDp; //接收数据包对象 24 25 String serverHost = "127.0.0.1"; //服务器IP 26 27 int serverPort = 10012; //服务器端口号 28 29 try{ 30 31 //建立连接 32 33 ds = new DatagramSocket(); 34 35 //初始化 36 37 InetAddress address = InetAddress.getByName(serverHost); 38 39 byte[] b = new byte[1024]; 40 41 receiveDp = new DatagramPacket(b,b.length); 42 43 System.out.println("客户端准备完成"); 44 45 //循环30次,每次间隔0.01秒 46 47 for(int i = 0;i < 30;i++){ 48 49 //初始化发送数据 50 51 Date d = new Date(); //当前时间 52 53 String content = d.toString(); //转换为字符串 54 55 byte[] data = content.getBytes(); 56 57 //初始化发送包对象 58 59 sendDp = new DatagramPacket(data,data.length,address, serverPort); 60 61 //发送 62 63 ds.send(sendDp); 64 65 //延迟 66 67 Thread.sleep(10); 68 69 //接收 70 71 ds.receive(receiveDp); 72 73 //读取反馈内容,并输出 74 75 byte[] response = receiveDp.getData(); 76 77 int len = receiveDp.getLength(); 78 79 String s = new String(response,0,len); 80 81 System.out.println("服务器端反馈为:" + s); 82 83 } 84 85 }catch(Exception e){ 86 87 e.printStackTrace(); 88 89 }finally{ 90 91 try{ 92 93 //关闭连接 94 95 ds.close(); 96 97 }catch(Exception e){} 98 99 } 100 101 } 102 103 }
在该示例中,将和服务器端进行数据交换的逻辑写在一个for循环的内部,这样就可以实现和服务器端的多次交换了,考虑到服务器端的响应速度,在每次发送之间加入0.01秒的时间间隔。最后当数据交换完成以后关闭连接,结束程序。
实现该逻辑的服务器端程序代码如下:
1 package udp; 2 3 import java.net.*; 4 5 /** 6 7 * 可以并发处理数据包的服务器端 8 9 * 功能为:显示客户端发送的内容,并向客户端反馈字符串“OK” 10 11 */ 12 13 public class MulUDPServer { 14 15 public static void main(String[] args) { 16 17 DatagramSocket ds = null; //连接对象 18 19 DatagramPacket receiveDp; //接收数据包对象 20 21 final int PORT = 10012; //端口 22 23 byte[] b = new byte[1024]; 24 25 receiveDp = new DatagramPacket(b,b.length); 26 27 try{ 28 29 //建立连接,监听端口 30 31 ds = new DatagramSocket(PORT); 32 33 System.out.println("服务器端已启动:"); 34 35 while(true){ 36 37 //接收 38 39 ds.receive(receiveDp); 40 41 //启动线程处理数据包 42 43 new LogicThread(ds,receiveDp); 44 45 } 46 47 }catch(Exception e){ 48 49 e.printStackTrace(); 50 51 }finally{ 52 53 try{ 54 55 //关闭连接 56 57 ds.close(); 58 59 }catch(Exception e){} 60 61 } 62 63 } 64 65 }
该代码实现了服务器端的接收逻辑,使用一个循环来接收客户端发送过来的数据包,当接收到数据包以后启动一个LogicThread线程处理该数据包。这样服务器端就可以实现同时处理多个数据包了,实现逻辑处理的线程代码如下:
1 package udp; 2 3 import java.net.*; 4 5 /** 6 7 * 逻辑处理线程 8 9 */ 10 11 public class LogicThread extends Thread { 12 13 /**连接对象*/ 14 15 DatagramSocket ds; 16 17 /**接收到的数据包*/ 18 19 DatagramPacket dp; 20 21 22 23 public LogicThread(DatagramSocket ds,DatagramPacket dp){ 24 25 this.ds = ds; 26 27 this.dp = dp; 28 29 start(); //启动线程 30 31 } 32 33 34 35 public void run(){ 36 37 try{ 38 39 //获得缓冲数组 40 41 byte[] data = dp.getData(); 42 43 //获得有效数据长度 44 45 int len = dp.getLength(); 46 47 //客户端IP 48 49 InetAddress clientAddress = dp.getAddress(); 50 51 //客户端端口 52 53 int clientPort = dp.getPort(); 54 55 //输出 56 57 System.out.println("客户端IP:" + clientAddress.getHostAddress()); 58 59 System.out.println("客户端端口号:" + clientPort); 60 61 System.out.println("客户端发送内容:" + new String(data,0,len)); 62 63 //反馈到客户端 64 65 byte[] b = "OK".getBytes(); 66 67 DatagramPacket sendDp = new DatagramPacket(b,b.length,clientAddress,clientPort); 68 69 //发送 70 71 ds.send(sendDp); 72 73 }catch(Exception e){ 74 75 e.printStackTrace(); 76 77 } 78 79 } 80 81 }
在该线程中,只处理一次UDP通讯,当通讯结束以后线程死亡,在线程内部,每次获得客户端发送过来的信息,将获得的信息输出到服务器端程序的控制台,然后向客户端反馈字符串“OK”。由于UDP数据传输过程中可能存在丢失,所以在运行该程序时可能会出现程序阻塞的情况。如果需要避免该问题,可以将客户端的网络发送部分也修改成线程实现。
网络协议
对于需要从事网络编程的程序员来说,网络协议是一个需要深刻理解的概念。那么什么是网络协议呢?
网络协议是指对于网络中传输的数据格式的规定。对于网络编程初学者来说,没有必要深入了解TCP/IP协议簇,所以对于初学者来说去读大部头的《TCP/IP协议》也不是一件很合适的事情,因为深入了解TCP/IP协议是网络编程提高阶段,也是深入网络编程底层时才需要做的事情。对于一般的网络编程来说,更多的是关心网络上传输的逻辑数据内容,也就是更多的是应用层上的网络协议,所以后续的内容均以实际应用的数据为基础来介绍网络协议的概念。
那么什么是网络协议呢,下面看一个简单的例子。春节晚会上“小沈阳”和赵本山合作的小品《不差钱》中,小沈阳和赵本山之间就设计了一个协议,协议的内容为:
如果点的菜价钱比较贵是,就说没有。
按照该协议的规定,就有了下面的对话:
赵本山:4斤的龙虾
小沈阳:(经过判断,得出价格比较高),没有
赵本山:鲍鱼
小沈阳:(经过判断,得出价格比较高),没有
这就是一种双方达成的一种协议约定,其实这种约定的实质和网络协议的实质是一样的。网络协议的实质也是客户端程序和服务器端程序对于数据的一种约定,只是由于以计算机为基础,所以更多的是使用数字来代表内容,这样就显得比较抽象一些。
下面再举一个简单的例子,介绍一些基础的网络协议设计的知识。例如需要设计一个简单的网络程序:网络计算器。也就是在客户端输入需要计算的数字和运算符,在 服务器端实现计算,并将计算的结果反馈给客户端。在这个例子中,就需要约定两个数据格式:客户端发送给服务器端的数据格式,以及服务器端反馈给客户端的数 据格式。
可能你觉得这个比较简单,例如客户端输入的数字依次是12和432,输入的运算符是加号,可能最容易想到的数据格式是形成字符串“12+432”,这样格式的确比较容易阅读,但是服务器端在进行计算时,逻辑就比较麻烦,因为需要首先拆分该字符串,然后才能进行计算,所以可用的数据格式就有了一下几种:
“12,432,+” 格式为:第一个数字,第二个数字,运算符
“12,+,432” 格式为:第一个数字,运算符,第二个数字
其实以上两种数据格式很接近,比较容易阅读,在服务器端收到该数据格式以后,使用“,”为分隔符分割字符串即可。
假设对于运算符再进行一次约定,例如约定数字0代表+,1代表减,2代表乘,3代表除,整体格式遵循以上第一种格式,则上面的数字生产的协议数据为:
“12,432,0”
这就是一种基本的发送的协议约定了。
另外一个需要设计的协议格式就是服务器端反馈的数据格式,其实服务器端主要反馈计算结果,但是在实际接受数据时,有可能存在格式错误的情况,这样就需要简单 的设计一下服务器端反馈的数据格式了。例如规定,如果发送的数据格式正确,则反馈结果,否则反馈字符串“错误”。这样就有了以下的数据格式:
客户端:“1,111,1” 服务器端:”-110”
客户端:“123,23,0” 服务器端:“146”
客户端:“1,2,5” 服务器端:“错误”
这样就设计出了一种最最基本的网络协议格式,从该示例中可以看出,网络协议就是一种格式上的约定,可以根据逻辑的需要约定出各种数据格式,在进行设计时一般遵循“简单、通用、容易解析”的原则进行。
而对于复杂的网络程序来说,需要传输的数据种类和数据量都比较大,这样只需要依次设计出每种情况下的数据格式即可,例如QQ程序,在该程序中需要进行传输的网络数据种类很多,那么在设计时就可以遵循:登录格式、注册格式、发送消息格式等等,一一进行设计即可。所以对于复杂的网络程序来说,只是增加了更多的命令格式,在实际设计时的工作量增加不是太大。
不管怎么说,在网络编程中,对于同一个网络程序来说,一般都会涉及到两个网络协议格式:客户端发送数据格式和服务器端反馈数据格式,在实际设计时,需要一一对应。这就是最基本的网络协议的知识。
网络协议设计完成以后,在进行网络编程时,就需要根据设计好的协议格式,在程序中进行对应的编码了,客户端程序和服务器端程序需要进行协议处理的代码分别如下。
客户端程序需要完成的处理为:
1、 客户端发送协议格式的生成
2、 服务器端反馈数据格式的解析
服务器端程序需要完成的处理为:
1、 服务器端反馈协议格式的生成
2、 客户端发送协议格式的解析
这里的生成是指将计算好的数据,转换成规定的数据格式,这里的解析指,从反馈的数据格式中拆分出需要的数据。在进行对应的代码编写时,严格遵循协议约定即可。所以,对于程序员来说,在进行网络程序编写时,需要首先根据逻辑的需要设计网络协议格式,然后遵循协议格式约定进行协议生成和解析代码的编写,最后使用网络编程技术实现整个网络编程的功能。由于各种网络程序使用不同的协议格式,所以不同网络程序的客户端之间无法通用。
而对于常见协议的格式,例如HTTP(Hyper Text Transfer Protocol,超文本传输协议)、FTP(File Transfer Protocol,文件传输协议),SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)等等,都有通用的规定,具体可以查阅相关的RFC文档。最后,对于一种网络程序来说,网络协议格式是该程序最核心的技术秘密,因为一旦协议格式泄漏,则任何一个人都可以根据该格式进行客户端的编写,这样将影响服务器端的实现,也容易出现一些其它的影响。
13.2.6小结
关于网络编程基本的技术就介绍这么多,该部分介绍了网络编程的基础知识,以及Java语言对于网络编程的支持,网络编程的步骤等,并详细介绍了TCP方式网络编程和UDP方式网络编程在Java语言中的实现,网络协议也是网络程序的核心,所以在实际开始进行网络编程时,设计一个良好的协议格式也是必须进行的工作。
网络编程示例
“实践出真知”,所以在进行技术学习时,还是需要进行很多的练习,才可以体会技术的奥妙,下面通过两个简单的示例,演示网络编程的实际使用。
13.3.1质数判别示例
该示例实现的功能是质数判断,程序实现的功能为客户端程序接收用户输入的数字,然后将用户输入的内容发送给服务器端,服务器端判断客户端发送的数字是否是质数,并将判断的结果反馈给客户端,客户端根据服务器端的反馈显示判断结果,质数的规则是:最小的质数是2,只能被1和自身整除的自然数。当用户输入小于2的数字,以及输入的内容不是自然数时,都属于非法输入,网络程序的功能都分为客户端程序和服务器端程序实现,下面先描述一下每个程序分别实现的功能:
1、 客户端程序功能:
a) 接收用户控制台输入
b) 判断输入内容是否合法
c) 按照协议格式生成发送数据
d) 发送数据
e) 接收服务器端反馈
f) 解析服务器端反馈信息,并输出
2、 服务器端程序功能:
a) 接收客户端发送数据
b) 按照协议格式解析数据
c) 判断数字是否是质数
d) 根据判断结果,生成协议数据
e) 将数据反馈给客户端
分解好了网络程序的功能以后,就可以设计网络协议格式了,如果该程序的功能比较简单,所以设计出的协议格式也不复杂。
客户端发送协议格式:
将用户输入的数字转换为字符串,再将字符串转换为byte数组即可。
例如用户输入16,则转换为字符串“16”,使用getBytes转换为byte数组。
客户端发送“quit”字符串代表结束连接
服务器端发送协议格式:
反馈数据长度为1个字节。数字0代表是质数,1代表不是质数,2代表协议格式错误。
例如客户端发送数字12,则反馈1,发送13则反馈0,发送0则反馈2。
功能设计完成以后,就可以分别进行客户端和服务器端程序的编写了,在编写完成以后联合起来进行调试即可。
下面分别以TCP方式和UDP方式实现该程序,注意其实现上的差异。不管使用哪种方式实现,客户端都可以多次输入数据进行判断。对于UDP方式来说,不需要向服务器端发送quit字符串。
以TCP方式实现的客户端程序代码如下:
1 package example1; 2 3 import java.io.*; 4 5 import java.net.*; 6 7 /** 8 9 * 以TCP方式实现的质数判断客户端程序 10 11 */ 12 13 public class TCPPrimeClient { 14 15 static BufferedReader br; 16 17 static Socket socket; 18 19 static InputStream is; 20 21 static OutputStream os; 22 23 /**服务器IP*/ 24 25 final static String HOST = "127.0.0.1"; 26 27 /**服务器端端口*/ 28 29 final static int PORT = 10005; 30 31 32 33 public static void main(String[] args) { 34 35 init(); //初始化 36 37 while(true){ 38 39 System.out.println("请输入数字:"); 40 41 String input = readInput(); //读取输入 42 43 if(isQuit(input)){ //判读是否结束 44 45 byte[] b = "quit".getBytes(); 46 47 send(b); 48 49 break; //结束程序 50 51 } 52 53 if(checkInput(input)){ //校验合法 54 55 //发送数据 56 57 send(input.getBytes()); 58 59 //接收数据 60 61 byte[] data = receive(); 62 63 //解析反馈数据 64 65 parse(data); 66 67 }else{ 68 69 System.out.println("输入不合法,请重新输入!"); 70 71 } 72 73 } 74 75 close(); //关闭流和连接 76 77 } 78 79 80 81 /** 82 83 * 初始化 84 85 */ 86 87 private static void init(){ 88 89 try { 90 91 br = new BufferedReader( 92 93 new InputStreamReader(System.in)); 94 95 socket = new Socket(HOST,PORT); 96 97 is = socket.getInputStream(); 98 99 os = socket.getOutputStream(); 100 101 } catch (Exception e) {} 102 103 } 104 105 106 107 /** 108 109 * 读取客户端输入 110 111 */ 112 113 private static String readInput(){ 114 115 try { 116 117 return br.readLine(); 118 119 } catch (Exception e) { 120 121 return null; 122 123 } 124 125 } 126 127 128 129 /** 130 131 * 判断是否输入quit 132 133 * @param input 输入内容 134 135 * @return true代表结束,false代表不结束 136 137 */ 138 139 private static boolean isQuit(String input){ 140 141 if(input == null){ 142 143 return false; 144 145 }else{ 146 147 if("quit".equalsIgnoreCase(input)){ 148 149 return true; 150 151 }else{ 152 153 return false; 154 155 } 156 157 } 158 159 } 160 161 162 163 /** 164 165 * 校验输入 166 167 * @param input 用户输入内容 168 169 * @return true代表输入符合要求,false代表不符合 170 171 */ 172 173 private static boolean checkInput(String input){ 174 175 if(input == null){ 176 177 return false; 178 179 } 180 181 try{ 182 183 int n = Integer.parseInt(input); 184 185 if(n >= 2){ 186 187 return true; 188 189 }else{ 190 191 return false; 192 193 } 194 195 }catch(Exception e){ 196 197 return false; //输入不是整数 198 199 } 200 201 } 202 203 204 205 /** 206 207 * 向服务器端发送数据 208 209 * @param data 数据内容 210 211 */ 212 213 private static void send(byte[] data){ 214 215 try{ 216 217 os.write(data); 218 219 }catch(Exception e){} 220 221 } 222 223 224 225 /** 226 227 * 接收服务器端反馈 228 229 * @return 反馈数据 230 231 */ 232 233 private static byte[] receive(){ 234 235 byte[] b = new byte[1024]; 236 237 try { 238 239 int n = is.read(b); 240 241 byte[] data = new byte[n]; 242 243 //复制有效数据 244 245 System.arraycopy(b, 0, data, 0, n); 246 247 return data; 248 249 } catch (Exception e){} 250 251 return null; 252 253 } 254 255 256 257 /** 258 259 * 解析协议数据 260 261 * @param data 协议数据 262 263 */ 264 265 private static void parse(byte[] data){ 266 267 if(data == null){ 268 269 System.out.println("服务器端反馈数据不正确!"); 270 271 return; 272 273 } 274 275 byte value = data[0]; //取第一个byte 276 277 //按照协议格式解析 278 279 switch(value){ 280 281 case 0: 282 283 System.out.println("质数"); 284 285 break; 286 287 case 1: 288 289 System.out.println("不是质数"); 290 291 break; 292 293 case 2: 294 295 System.out.println("协议格式错误"); 296 297 break; 298 299 } 300 301 } 302 303 304 305 /** 306 307 * 关闭流和连接 308 309 */ 310 311 private static void close(){ 312 313 try{ 314 315 br.close(); 316 317 is.close(); 318 319 os.close(); 320 321 socket.close(); 322 323 }catch(Exception e){ 324 325 e.printStackTrace(); 326 327 } 328 329 } 330 331 } 332 333 在该代码中,将程序的功能使用方法进行组织,使得结构比较清晰,核心的逻辑流程在main方法中实现。 334 335 以TCP方式实现的服务器端的代码如下: 336 337 package example1; 338 339 import java.net.*; 340 341 /** 342 343 * 以TCP方式实现的质数判别服务器端 344 345 */ 346 347 public class TCPPrimeServer { 348 349 public static void main(String[] args) { 350 351 final int PORT = 10005; 352 353 ServerSocket ss = null; 354 355 try { 356 357 ss = new ServerSocket(PORT); 358 359 System.out.println("服务器端已启动:"); 360 361 while(true){ 362 363 Socket s = ss.accept(); 364 365 new PrimeLogicThread(s); 366 367 } 368 369 } catch (Exception e) {} 370 371 finally{ 372 373 try { 374 375 ss.close(); 376 377 } catch (Exception e2) {} 378 379 } 380 381 382 383 } 384 385 } 386 387 package example1; 388 389 import java.io.*; 390 391 import java.net.*; 392 393 /** 394 395 * 实现质数判别逻辑的线程 396 397 */ 398 399 public class PrimeLogicThread extends Thread { 400 401 Socket socket; 402 403 InputStream is; 404 405 OutputStream os; 406 407 408 409 public PrimeLogicThread(Socket socket){ 410 411 this.socket = socket; 412 413 init(); 414 415 start(); 416 417 } 418 419 /** 420 421 * 初始化 422 423 */ 424 425 private void init(){ 426 427 try{ 428 429 is = socket.getInputStream(); 430 431 os = socket.getOutputStream(); 432 433 }catch(Exception e){} 434 435 } 436 437 438 439 public void run(){ 440 441 while(true){ 442 443 //接收客户端反馈 444 445 byte[] data = receive(); 446 447 //判断是否是退出 448 449 if(isQuit(data)){ 450 451 break; //结束循环 452 453 } 454 455 //逻辑处理 456 457 byte[] b = logic(data); 458 459 //反馈数据 460 461 send(b); 462 463 } 464 465 close(); 466 467 } 468 469 470 471 /** 472 473 * 接收客户端数据 474 475 * @return 客户端发送的数据 476 477 */ 478 479 private byte[] receive(){ 480 481 byte[] b = new byte[1024]; 482 483 try { 484 485 int n = is.read(b); 486 487 byte[] data = new byte[n]; 488 489 //复制有效数据 490 491 System.arraycopy(b, 0, data, 0, n); 492 493 return data; 494 495 } catch (Exception e){} 496 497 return null; 498 499 } 500 501 502 503 /** 504 505 * 向客户端发送数据 506 507 * @param data 数据内容 508 509 */ 510 511 private void send(byte[] data){ 512 513 try{ 514 515 os.write(data); 516 517 }catch(Exception e){} 518 519 } 520 521 522 523 /** 524 525 * 判断是否是quit 526 527 * @return 是返回true,否则返回false 528 529 */ 530 531 private boolean isQuit(byte[] data){ 532 533 if(data == null){ 534 535 return false; 536 537 }else{ 538 539 String s = new String(data); 540 541 if(s.equalsIgnoreCase("quit")){ 542 543 return true; 544 545 }else{ 546 547 return false; 548 549 } 550 551 } 552 553 } 554 555 556 557 private byte[] logic(byte[] data){ 558 559 //反馈数组 560 561 byte[] b = new byte[1]; 562 563 //校验参数 564 565 if(data == null){ 566 567 b[0] = 2; 568 569 return b; 570 571 } 572 573 try{ 574 575 //转换为数字 576 577 String s = new String(data); 578 579 int n = Integer.parseInt(s); 580 581 //判断是否是质数 582 583 if(n >= 2){ 584 585 boolean flag = isPrime(n); 586 587 if(flag){ 588 589 b[0] = 0; 590 591 }else{ 592 593 b[0] = 1; 594 595 } 596 597 }else{ 598 599 b[0] = 2; //格式错误 600 601 System.out.println(n); 602 603 } 604 605 }catch(Exception e){ 606 607 e.printStackTrace(); 608 609 b[0] = 2; 610 611 } 612 613 return b; 614 615 } 616 617 618 619 /** 620 621 * 622 623 * @param n 624 625 * @return 626 627 */ 628 629 private boolean isPrime(int n){ 630 631 boolean b = true; 632 633 for(int i = 2;i <= Math.sqrt(n);i++){ 634 635 if(n % i == 0){ 636 637 b = false; 638 639 break; 640 641 } 642 643 } 644 645 return b; 646 647 } 648 649 650 651 /** 652 653 * 关闭连接 654 655 */ 656 657 private void close(){ 658 659 try { 660 661 is.close(); 662 663 os.close(); 664 665 socket.close(); 666 667 } catch (Exception e){} 668 669 } 670 671 }
本示例使用的服务器端的结构和前面示例中的结构一致,只是逻辑线程的实现相对来说要复杂一些,在线程类中的logic方法中实现了服务器端逻辑,根据客户端发送过来的数据,判断是否是质数,然后根据判断结果按照协议格式要求,生成客户端反馈数据,实现服务器端要求的功能。
猜数字小游戏
下面这个示例是一个猜数字的控制台小游戏。该游戏的规则是:当客户端第一次连接到服务器端时,服务器端生产一个【0,50】之间的随机数字,然后客户端输入数字来猜该数字,每次客户端输入数字以后,发送给服务器端,服务器端判断该客户端发送的数字和随机数字的关系,并反馈比较结果,客户端总共有5次猜的机会,猜中时提示猜中,当输入”quit”时结束程序。
和前面的示例类似,在进行网络程序开发时,首先需要分解一下功能的实现,觉得功能是在客户端程序中实现还是在服务器端程序中实现。区分的规则一般是:客户端 程序实现接收用户输入等界面功能,并实现一些基础的校验降低服务器端的压力,而将程序核心的逻辑以及数据存储等功能放在服务器端进行实现。遵循该原则划分 的客户端和服务器端功能如下所示。
客户端程序功能列表:
1、 接收用户控制台输入
2、 判断输入内容是否合法
3、 按照协议格式发送数据
4、 根据服务器端的反馈给出相应提示
服务器端程序功能列表:
1、 接收客户端发送数据
2、 按照协议格式解析数据
3、 判断发送过来的数字和随机数字的关系
4、 根据判断结果生产协议数据
5、 将生产的数据反馈给客户端
在该示例中,实际使用的网络命令也只有两条,所以显得协议的格式比较简单。
其中客户端程序协议格式如下:
1、 将用户输入的数字转换为字符串,然后转换为byte数组
2、 发送“quit”字符串代表退出
其中服务器端程序协议格式如下:
1、 反馈长度为1个字节,数字0代表相等(猜中),1代表大了,2代表小了,其它数字代表错误。
实现该程序的代码比较多,下面分为客户端程序实现和服务器端程序实现分别进行列举。
客户端程序实现代码如下:
1 package guess; 2 3 import java.net.*; 4 5 import java.io.*; 6 7 /** 8 9 * 猜数字客户端 10 11 */ 12 13 public class TCPClient { 14 15 public static void main(String[] args) { 16 17 Socket socket = null; 18 19 OutputStream os = null; 20 21 InputStream is = null; 22 23 BufferedReader br = null; 24 25 byte[] data = new byte[2]; 26 27 try{ 28 29 //建立连接 30 31 socket = new Socket( 32 33 "127.0.0.1",10001); 34 35 36 37 //发送数据 38 39 os= socket.getOutputStream(); 40 41 42 43 //读取反馈数据 44 45 is = socket.getInputStream(); 46 47 48 49 //键盘输入流 50 51 br = new BufferedReader( 52 53 new InputStreamReader(System.in)); 54 55 56 57 //多次输入 58 59 while(true){ 60 61 System.out.println("请输入数字:"); 62 63 //接收输入 64 65 String s = br.readLine(); 66 67 //结束条件 68 69 if(s.equals("quit")){ 70 71 os.write("quit".getBytes()); 72 73 break; 74 75 } 76 77 //校验输入是否合法 78 79 boolean b = true; 80 81 try{ 82 83 Integer.parseInt(s); 84 85 }catch(Exception e){ 86 87 b = false; 88 89 } 90 91 if(b){ //输入合法 92 93 //发送数据 94 95 os.write(s.getBytes()); 96 97 //接收反馈 98 99 is.read(data); 100 101 //判断 102 103 switch(data[0]){ 104 105 case 0: 106 107 System.out.println("相等!祝贺你!"); 108 109 break; 110 111 case 1: 112 113 System.out.println("大了!"); 114 115 break; 116 117 case 2: 118 119 System.out.println("小了!"); 120 121 break; 122 123 default: 124 125 System.out.println("其它错误!"); 126 127 } 128 129 //提示猜的次数 130 131 System.out.println("你已经猜了" + data[1] + "次!"); 132 133 //判断次数是否达到5次 134 135 if(data[1] >= 5){ 136 137 System.out.println("你挂了!"); 138 139 //给服务器端线程关闭的机会 140 141 os.write("quit".getBytes()); 142 143 //结束客户端程序 144 145 break; 146 147 } 148 149 }else{ //输入错误 150 151 System.out.println("输入错误!"); 152 153 } 154 155 } 156 157 }catch(Exception e){ 158 159 e.printStackTrace(); 160 161 }finally{ 162 163 try{ 164 165 //关闭连接 166 167 br.close(); 168 169 is.close(); 170 171 os.close(); 172 173 socket.close(); 174 175 }catch(Exception e){ 176 177 e.printStackTrace(); 178 179 } 180 181 } 182 183 } 184 185 } 186 187 在该示例中,首先建立一个到IP地址为127.0.0.1的端口为10001的连接,然后进行各个流的初始化工作,将逻辑控制的代码放入在一个while循环中,这样可以在客户端多次进行输入。在循环内部,首先判断用户输入的是否为quit字符串,如果是则结束程序,如果输入不是quit,则首先校验输入的是否是数字,如果不是数字则直接输出“输入错误!”并继续接收用户输入,如果是数字则发送给服务器端,并根据服务器端的反馈显示相应的提示信息。最后关闭流和连接,结束客户端程序。 188 189 服务器端程序的实现还是分为服务器控制程序和逻辑线程,实现的代码分别如下: 190 191 package guess; 192 193 import java.net.*; 194 195 /** 196 197 * TCP连接方式的服务器端 198 199 * 实现功能:接收客户端的数据,判断数字关系 200 201 */ 202 203 public class TCPServer { 204 205 public static void main(String[] args) { 206 207 try{ 208 209 //监听端口 210 211 ServerSocket ss = new ServerSocket(10001); 212 213 System.out.println("服务器已启动:"); 214 215 //逻辑处理 216 217 while(true){ 218 219 //获得连接 220 221 Socket s = ss.accept(); 222 223 //启动线程处理 224 225 new LogicThread(s); 226 227 } 228 229 230 231 }catch(Exception e){ 232 233 e.printStackTrace(); 234 235 } 236 237 } 238 239 } 240 241 package guess; 242 243 import java.net.*; 244 245 import java.io.*; 246 247 import java.util.*; 248 249 /** 250 251 * 逻辑处理线程 252 253 */ 254 255 public class LogicThread extends Thread { 256 257 Socket s; 258 259 260 261 static Random r = new Random(); 262 263 264 265 public LogicThread(Socket s){ 266 267 this.s = s; 268 269 start(); //启动线程 270 271 } 272 273 274 275 public void run(){ 276 277 //生成一个[0,50]的随机数 278 279 int randomNumber = Math.abs(r.nextInt() % 51); 280 281 //用户猜的次数 282 283 int guessNumber = 0; 284 285 InputStream is = null; 286 287 OutputStream os = null; 288 289 byte[] data = new byte[2]; 290 291 try{ 292 293 //获得输入流 294 295 is = s.getInputStream(); 296 297 //获得输出流 298 299 os = s.getOutputStream(); 300 301 while(true){ //多次处理 302 303 //读取客户端发送的数据 304 305 byte[] b = new byte[1024]; 306 307 int n = is.read(b); 308 309 String send = new String(b,0,n); 310 311 //结束判别 312 313 if(send.equals("quit")){ 314 315 break; 316 317 } 318 319 //解析、判断 320 321 try{ 322 323 int num = Integer.parseInt(send); 324 325 //处理 326 327 guessNumber++; //猜的次数增加1 328 329 data[1] = (byte)guessNumber; 330 331 //判断 332 333 if(num > randomNumber){ 334 335 data[0] = 1; 336 337 }else if(num < randomNumber){ 338 339 data[0] = 2; 340 341 }else{ 342 343 data[0] = 0; 344 345 //如果猜对 346 347 guessNumber = 0; //清零 348 349 randomNumber = Math.abs(r.nextInt() % 51); 350 351 } 352 353 //反馈给客户端 354 355 os.write(data); 356 357 358 359 }catch(Exception e){ //数据格式错误 360 361 data[0] = 3; 362 363 data[1] = (byte)guessNumber; 364 365 os.write(data); //发送错误标识 366 367 break; 368 369 } 370 371 os.flush(); //强制发送 372 373 } 374 375 376 377 }catch(Exception e){ 378 379 e.printStackTrace(); 380 381 }finally{ 382 383 try{ 384 385 is.close(); 386 387 os.close(); 388 389 s.close(); 390 391 }catch(Exception e){} 392 393 } 394 395 } 396 397 }
在该示例中,服务器端控制部分和前面的示例中一样。也是等待客户端连接,如果有客户端连接到达时,则启动新的线程去处理客户端连接。在逻辑线程中实现程序的 核心逻辑,首先当线程执行时生产一个随机数字,然后根据客户端发送过来的数据,判断客户端发送数字和随机数字的关系,然后反馈相应的数字的值,并记忆客户 端已经猜过的次数,当客户端猜中以后清零猜过的次数,使得客户端程序可以继续进行游戏。
总体来说,该程序示例的结构以及功能都与上一个程序比较类似,希望通过比较这两个程序,加深对于网络编程的认识,早日步入网络编程的大门。