Java语言是在网络环境下诞生的,因此Java语言虽然不能说是对于网络编程的支持最好的语言,可是必须说是一种对于网络编程提供良好支持的语言,使用Java语言进行网络编程将是一件比较轻松的工做。java
和网络编程有关的基本API位于java.net包中,该包中包含了基本的网络编程实现,该包是网络编程的基础。该包中既包含基础的网络编程类,也包含封装后的专门处理WEB相关的处理类。在本章中,将只介绍基础的网络编程类。程序员
首先来介绍一个基础的网络类——InetAddress类。该类的功能是表明一个IP地址,而且将IP地址和域名相关的操做方法包含在该类的内部。编程
关于该类的使用,下面经过一个基础的代码示例演示该类的使用,代码以下:数组
package inetaddressdemo;服务器
import java.net.*;网络
/**并发
* 演示InetAddress类的基本使用dom
*/socket
public class InetAddressDemo {tcp
public static void main(String[] args) {
try{
//使用域名建立对象
InetAddress inet1 = InetAddress.getByName("www.163.com");
System.out.println(inet1);
//使用IP建立对象
InetAddress inet2 = InetAddress.getByName("127.0.0.1");
System.out.println(inet2);
//得到本机地址对象
InetAddress inet3 = InetAddress.getLocalHost();
System.out.println(inet3);
//得到对象中存储的域名
String host = inet3.getHostName();
System.out.println("域名:" + host);
//得到对象中存储的IP
String ip = inet3.getHostAddress();
System.out.println("IP:" + ip);
}catch(Exception e){}
}
}
在该示例代码中,演示了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地址进行实现。
按照前面的介绍,网络通信的方式有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”,并将服务器端的反馈显示到控制台,数据交换只进行一次,当数据交换进行完成之后关闭网络链接,程序结束。实现的代码以下:
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,端口号码为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服务器端实现的功能就是将客户端发送的内容再原封不动的反馈给客户端。实现的代码以下:
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语言中的基本实现,这个示例只是演示了网络编程的基本步骤以及各个功能方法的基本使用,只是为网络编程打下了一个基础,下面将就几个问题来深刻介绍网络编程深层次的一些知识。
为了一步一步的掌握网络编程,下面再研究网络编程中的两个基本问题,经过解决这两个问题将对网络编程的认识深刻一层。
一、如何复用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,表示客户端退出呢,这就涉及到网络协议的内容了,会在后续的网络应用示例部分详细介绍。下面开始介绍另一个网络编程的突出问题。
二、如何使服务器端支持多个客户端同时工做?
前面介绍的服务器端程序,只是实现了概念上的服务器端,离实际的服务器端程序结构距离还很遥远,若是须要让服务器端可以实际使用,那么最须要解决的问题就是——如何支持多个客户端同时工做。
一个服务器端通常都须要同时为多个客户端提供通信,若是须要同时支持多个客户端,则必须使用前面介绍的线程的概念。简单来讲,也就是当服务器端接收到一个链接时,启动一个专门的线程处理和该客户端的通信。
按照这个思路改写的服务端示例程序将由两个部分组成,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方式的编程,包含客户端网络编程和服务器端网络编程,主要由两个类实现,分别是:
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类型的对象内部。实现接收数据以及显示服务器端反馈内容的示例代码以下:
//构造缓冲数组
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是预估的接收到的数据长度,要求该长度必须大于或等于接收到的数据长度,而后以该缓冲数组为基础,构造数据包对象,使用链接对象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”。实现该功能的客户端代码以下所示:
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和端口,构形成发送数据包对象,调用链接对象ds的send方法发送出去。在数据发送出去之后,构造接收数据的数据包对象,调用链接对象ds的receive方法接收服务器端的反馈,并输出在控制台。最后在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网络编程就介绍这么多了,下面将介绍一下网络协议的概念。
网络协议
对于须要从事网络编程的程序员来讲,网络协议是一个须要深入理解的概念。那么什么是网络协议呢?
网络协议是指对于网络中传输的数据格式的规定。对于网络编程初学者来讲,没有必要深刻了解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程序,在该程序中须要进行传输的网络数据种类不少,那么在设计时就能够遵循:登陆格式、注册格式、发送消息格式等等,一一进行设计便可。因此对于复杂的网络程序来讲,只是增长了更多的命令格式,在实际设计时的工做量增长不是太大。
无论怎么说,在网络编程中,对于同一个网络程序来讲,通常都会涉及到两个网络协议格式:客户端发送数据格式和服务器端反馈数据格式,在实际设计时,须要一一对应。这就是最基本的网络协议的知识。
网络协议设计完成之后,在进行网络编程时,就须要根据设计好的协议格式,在程序中进行对应的编码了,客户端程序和服务器端程序须要进行协议处理的代码分别以下。
客户端程序须要完成的处理为:
一、 客户端发送协议格式的生成
二、 服务器端反馈数据格式的解析
服务器端程序须要完成的处理为:
一、 服务器端反馈协议格式的生成
二、 客户端发送协议格式的解析
这里的生成是指将计算好的数据,转换成规定的数据格式,这里的解析指,从反馈的数据格式中拆分出须要的数据。在进行对应的代码编写时,严格遵循协议约定便可。
因此,对于程序员来讲,在进行网络程序编写时,须要首先根据逻辑的须要设计网络协议格式,而后遵循协议格式约定进行协议生成和解析代码的编写,最后使用网络编程技术实现整个网络编程的功能。
因为各类网络程序使用不一样的协议格式,因此不一样网络程序的客户端之间没法通用。
而对于常见协议的格式,例如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的数字,以及输入的内容不是天然数时,都属于非法输入。
网络程序的功能都分为客户端程序和服务器端程序实现,下面先描述一下每一个程序分别实现的功能:
一、 客户端程序功能:
a) 接收用户控制台输入
b) 判断输入内容是否合法
c) 按照协议格式生成发送数据
d) 发送数据
e) 接收服务器端反馈
f) 解析服务器端反馈信息,并输出
二、 服务器端程序功能:
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方式实现的客户端程序代码以下:
package example1;
import java.io.*;
import java.net.*;
/**
* 以TCP方式实现的质数判断客户端程序
*/
public class TCPPrimeClient {
static BufferedReader br;
static Socket socket;
static InputStream is;
static OutputStream os;
/**服务器IP*/
final static String HOST = "127.0.0.1";
/**服务器端端口*/
final static int PORT = 10005;
public static void main(String[] args) {
init(); //初始化
while(true){
System.out.println("请输入数字:");
String input = readInput(); //读取输入
if(isQuit(input)){ //判读是否结束
byte[] b = "quit".getBytes();
send(b);
break; //结束程序
}
if(checkInput(input)){ //校验合法
//发送数据
send(input.getBytes());
//接收数据
byte[] data = receive();
//解析反馈数据
parse(data);
}else{
System.out.println("输入不合法,请从新输入!");
}
}
close(); //关闭流和链接
}
/**
* 初始化
*/
private static void init(){
try {
br = new BufferedReader(
new InputStreamReader(System.in));
socket = new Socket(HOST,PORT);
is = socket.getInputStream();
os = socket.getOutputStream();
} catch (Exception e) {}
}
/**
* 读取客户端输入
*/
private static String readInput(){
try {
return br.readLine();
} catch (Exception e) {
return null;
}
}
/**
* 判断是否输入quit
* @param input 输入内容
* @return true表明结束,false表明不结束
*/
private static boolean isQuit(String input){
if(input == null){
return false;
}else{
if("quit".equalsIgnoreCase(input)){
return true;
}else{
return false;
}
}
}
/**
* 校验输入
* @param input 用户输入内容
* @return true表明输入符合要求,false表明不符合
*/
private static boolean checkInput(String input){
if(input == null){
return false;
}
try{
int n = Integer.parseInt(input);
if(n >= 2){
return true;
}else{
return false;
}
}catch(Exception e){
return false; //输入不是整数
}
}
/**
* 向服务器端发送数据
* @param data 数据内容
*/
private static void send(byte[] data){
try{
os.write(data);
}catch(Exception e){}
}
/**
* 接收服务器端反馈
* @return 反馈数据
*/
private static byte[] receive(){
byte[] b = new byte[1024];
try {
int n = is.read(b);
byte[] data = new byte[n];
//复制有效数据
System.arraycopy(b, 0, data, 0, n);
return data;
} catch (Exception e){}
return null;
}
/**
* 解析协议数据
* @param data 协议数据
*/
private static void parse(byte[] data){
if(data == null){
System.out.println("服务器端反馈数据不正确!");
return;
}
byte value = data[0]; //取第一个byte
//按照协议格式解析
switch(value){
case 0:
System.out.println("质数");
break;
case 1:
System.out.println("不是质数");
break;
case 2:
System.out.println("协议格式错误");
break;
}
}
/**
* 关闭流和链接
*/
private static void close(){
try{
br.close();
is.close();
os.close();
socket.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
在该代码中,将程序的功能使用方法进行组织,使得结构比较清晰,核心的逻辑流程在main方法中实现。
以TCP方式实现的服务器端的代码以下:
package example1;
import java.net.*;
/**
* 以TCP方式实现的质数判别服务器端
*/
public class TCPPrimeServer {
public static void main(String[] args) {
final int PORT = 10005;
ServerSocket ss = null;
try {
ss = new ServerSocket(PORT);
System.out.println("服务器端已启动:");
while(true){
Socket s = ss.accept();
new PrimeLogicThread(s);
}
} catch (Exception e) {}
finally{
try {
ss.close();
} catch (Exception e2) {}
}
}
}
package example1;
import java.io.*;
import java.net.*;
/**
* 实现质数判别逻辑的线程
*/
public class PrimeLogicThread extends Thread {
Socket socket;
InputStream is;
OutputStream os;
public PrimeLogicThread(Socket socket){
this.socket = socket;
init();
start();
}
/**
* 初始化
*/
private void init(){
try{
is = socket.getInputStream();
os = socket.getOutputStream();
}catch(Exception e){}
}
public void run(){
while(true){
//接收客户端反馈
byte[] data = receive();
//判断是不是退出
if(isQuit(data)){
break; //结束循环
}
//逻辑处理
byte[] b = logic(data);
//反馈数据
send(b);
}
close();
}
/**
* 接收客户端数据
* @return 客户端发送的数据
*/
private byte[] receive(){
byte[] b = new byte[1024];
try {
int n = is.read(b);
byte[] data = new byte[n];
//复制有效数据
System.arraycopy(b, 0, data, 0, n);
return data;
} catch (Exception e){}
return null;
}
/**
* 向客户端发送数据
* @param data 数据内容
*/
private void send(byte[] data){
try{
os.write(data);
}catch(Exception e){}
}
/**
* 判断是不是quit
* @return 是返回true,不然返回false
*/
private boolean isQuit(byte[] data){
if(data == null){
return false;
}else{
String s = new String(data);
if(s.equalsIgnoreCase("quit")){
return true;
}else{
return false;
}
}
}
private byte[] logic(byte[] data){
//反馈数组
byte[] b = new byte[1];
//校验参数
if(data == null){
b[0] = 2;
return b;
}
try{
//转换为数字
String s = new String(data);
int n = Integer.parseInt(s);
//判断是不是质数
if(n >= 2){
boolean flag = isPrime(n);
if(flag){
b[0] = 0;
}else{
b[0] = 1;
}
}else{
b[0] = 2; //格式错误
System.out.println(n);
}
}catch(Exception e){
e.printStackTrace();
b[0] = 2;
}
return b;
}
/**
*
* @param n
* @return
*/
private boolean isPrime(int n){
boolean b = true;
for(int i = 2;i <= Math.sqrt(n);i++){
if(n % i == 0){
b = false;
break;
}
}
return b;
}
/**
* 关闭链接
*/
private void close(){
try {
is.close();
os.close();
socket.close();
} catch (Exception e){}
}
}
本示例使用的服务器端的结构和前面示例中的结构一致,只是逻辑线程的实现相对来讲要复杂一些,在线程类中的logic方法中实现了服务器端逻辑,根据客户端发送过来的数据,判断是不是质数,而后根据判断结果按照协议格式要求,生成客户端反馈数据,实现服务器端要求的功能。
猜数字小游戏
下面这个示例是一个猜数字的控制台小游戏。该游戏的规则是:当客户端第一次链接到服务器端时,服务器端生产一个【0,50】之间的随机数字,而后客户端输入数字来猜该数字,每次客户端输入数字之后,发送给服务器端,服务器端判断该客户端发送的数字和随机数字的关系,并反馈比较结果,客户端总共有5次猜的机会,猜中时提示猜中,当输入”quit”时结束程序。
和 前面的示例相似,在进行网络程序开发时,首先须要分解一下功能的实现,以为功能是在客户端程序中实现仍是在服务器端程序中实现。区分的规则通常是:客户端 程序实现接收用户输入等界面功能,并实现一些基础的校验下降服务器端的压力,而将程序核心的逻辑以及数据存储等功能放在服务器端进行实现。遵循该原则划分 的客户端和服务器端功能以下所示。
客户端程序功能列表:
一、 接收用户控制台输入
二、 判断输入内容是否合法
三、 按照协议格式发送数据
四、 根据服务器端的反馈给出相应提示
服务器端程序功能列表:
一、 接收客户端发送数据
二、 按照协议格式解析数据
三、 判断发送过来的数字和随机数字的关系
四、 根据判断结果生产协议数据
五、 将生产的数据反馈给客户端
在该示例中,实际使用的网络命令也只有两条,因此显得协议的格式比较简单。
其中客户端程序协议格式以下:
一、 将用户输入的数字转换为字符串,而后转换为byte数组
二、 发送“quit”字符串表明退出
其中服务器端程序协议格式以下:
一、 反馈长度为1个字节,数字0表明相等(猜中),1表明大了,2表明小了,其它数字表明错误。
实现该程序的代码比较多,下面分为客户端程序实现和服务器端程序实现分别进行列举。
客户端程序实现代码以下:
package guess;
import java.net.*;
import java.io.*;
/**
* 猜数字客户端
*/
public class TCPClient {
public static void main(String[] args) {
Socket socket = null;
OutputStream os = null;
InputStream is = null;
BufferedReader br = null;
byte[] data = new byte[2];
try{
//创建链接
socket = new Socket(
"127.0.0.1",10001);
//发送数据
os= socket.getOutputStream();
//读取反馈数据
is = socket.getInputStream();
//键盘输入流
br = new BufferedReader(
new InputStreamReader(System.in));
//屡次输入
while(true){
System.out.println("请输入数字:");
//接收输入
String s = br.readLine();
//结束条件
if(s.equals("quit")){
os.write("quit".getBytes());
break;
}
//校验输入是否合法
boolean b = true;
try{
Integer.parseInt(s);
}catch(Exception e){
b = false;
}
if(b){ //输入合法
//发送数据
os.write(s.getBytes());
//接收反馈
is.read(data);
//判断
switch(data[0]){
case 0:
System.out.println("相等!祝贺你!");
break;
case 1:
System.out.println("大了!");
break;
case 2:
System.out.println("小了!");
break;
default:
System.out.println("其它错误!");
}
//提示猜的次数
System.out.println("你已经猜了" + data[1] + "次!");
//判断次数是否达到5次
if(data[1] >= 5){
System.out.println("你挂了!");
//给服务器端线程关闭的机会
os.write("quit".getBytes());
//结束客户端程序
break;
}
}else{ //输入错误
System.out.println("输入错误!");
}
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//关闭链接
br.close();
is.close();
os.close();
socket.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
在该示例中,首先创建一个到IP地址为127.0.0.1的端口为10001的链接,而后进行各个流的初始化工做,将逻辑控制的代码放入在一个while循环中,这样能够在客户端屡次进行输入。在循环内部,首先判断用户输入的是否为quit字符串,若是是则结束程序,若是输入不是quit,则首先校验输入的是不是数字,若是不是数字则直接输出“输入错误!”并继续接收用户输入,若是是数字则发送给服务器端,并根据服务器端的反馈显示相应的提示信息。最后关闭流和链接,结束客户端程序。
服务器端程序的实现仍是分为服务器控制程序和逻辑线程,实现的代码分别以下:
package guess;
import java.net.*;
/**
* TCP链接方式的服务器端
* 实现功能:接收客户端的数据,判断数字关系
*/
public class TCPServer {
public static void main(String[] args) {
try{
//监听端口
ServerSocket ss = new ServerSocket(10001);
System.out.println("服务器已启动:");
//逻辑处理
while(true){
//得到链接
Socket s = ss.accept();
//启动线程处理
new LogicThread(s);
}
}catch(Exception e){
e.printStackTrace();
}
}
}
package guess;
import java.net.*;
import java.io.*;
import java.util.*;
/**
* 逻辑处理线程
*/
public class LogicThread extends Thread {
Socket s;
static Random r = new Random();
public LogicThread(Socket s){
this.s = s;
start(); //启动线程
}
public void run(){
//生成一个[0,50]的随机数
int randomNumber = Math.abs(r.nextInt() % 51);
//用户猜的次数
int guessNumber = 0;
InputStream is = null;
OutputStream os = null;
byte[] data = new byte[2];
try{
//得到输入流
is = s.getInputStream();
//得到输出流
os = s.getOutputStream();
while(true){ //屡次处理
//读取客户端发送的数据
byte[] b = new byte[1024];
int n = is.read(b);
String send = new String(b,0,n);
//结束判别
if(send.equals("quit")){
break;
}
//解析、判断
try{
int num = Integer.parseInt(send);
//处理
guessNumber++; //猜的次数增长1
data[1] = (byte)guessNumber;
//判断
if(num > randomNumber){
data[0] = 1;
}else if(num < randomNumber){
data[0] = 2;
}else{
data[0] = 0;
//若是猜对
guessNumber = 0; //清零
randomNumber = Math.abs(r.nextInt() % 51);
}
//反馈给客户端
os.write(data);
}catch(Exception e){ //数据格式错误
data[0] = 3;
data[1] = (byte)guessNumber;
os.write(data); //发送错误标识
break;
}
os.flush(); //强制发送
}
}catch(Exception e){
e.printStackTrace();
}finally{
try{
is.close();
os.close();
s.close();
}catch(Exception e){}
}
}
}
在 该示例中,服务器端控制部分和前面的示例中同样。也是等待客户端链接,若是有客户端链接到达时,则启动新的线程去处理客户端链接。在逻辑线程中实现程序的 核心逻辑,首先当线程执行时生产一个随机数字,而后根据客户端发送过来的数据,判断客户端发送数字和随机数字的关系,而后反馈相应的数字的值,并记忆客户 端已经猜过的次数,当客户端猜中之后清零猜过的次数,使得客户端程序能够继续进行游戏。