网络协议
对于需要从事网络编程的程序员来说,网络协议是一个需要深刻理解的概念。那么什么是网络协议呢?
网络协议是指对于网络中传输的数据格式的规定。对于网络编程初学者来说,没有必要深入了解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方式实现的客户端程序代码如下:
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”时结束程序。
和 前面的示例类似,在进行网络程序开发时,首先需要分解一下功能的实现,觉得功能是在客户端程序中实现还是在服务器端程序中实现。区分的规则一般是:客户端 程序实现接收用户输入等界面功能,并实现一些基础的校验降低服务器端的压力,而将程序核心的逻辑以及数据存储等功能放在服务器端进行实现。遵循该原则划分 的客户端和服务器端功能如下所示。
客户端程序功能列表:
1、 接收用户控制台输入
2、 判断输入内容是否合法
3、 按照协议格式发送数据
4、 根据服务器端的反馈给出相应提示
服务器端程序功能列表:
1、 接收客户端发送数据
2、 按照协议格式解析数据
3、 判断发送过来的数字和随机数字的关系
4、 根据判断结果生产协议数据
5、 将生产的数据反馈给客户端
在该示例中,实际使用的网络命令也只有两条,所以显得协议的格式比较简单。
其中客户端程序协议格式如下:
1、 将用户输入的数字转换为字符串,然后转换为byte数组
2、 发送“quit”字符串代表退出
其中服务器端程序协议格式如下:
1、 反馈长度为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){}
}
}
}
在 该示例中,服务器端控制部分和前面的示例中一样。也是等待客户端连接,如果有客户端连接到达时,则启动新的线程去处理客户端连接。在逻辑线程中实现程序的 核心逻辑,首先当线程执行时生产一个随机数字,然后根据客户端发送过来的数据,判断客户端发送数字和随机数字的关系,然后反馈相应的数字的值,并记忆客户 端已经猜过的次数,当客户端猜中以后清零猜过的次数,使得客户端程序可以继续进行游戏。
总体来说,该程序示例的结构以及功能都与上一个程序比较类似,希望通过比较这两个程序,加深对于网络编程的认识,早日步入网络编程的大门。