20162330 实验五 《网络编程与安全》 实验报告
课程名称:《程序设计与数据结构》
学生班级:1623班
学生姓名:刘伟康
学生学号:20162330
实验时间:2017年6月5日—2017年6月9日
实验名称:《网络编程与安全》
指导老师:娄嘉鹏、王志强老师
- 实验步骤
- 前期准备
- 需求分析
实验要求:
1.两人一组结对编程,(从第二个小实验开始)一人负责客户端,一人负责服务器。注意责任归宿,要会通过测试证明自己没有问题。上传测试代码运行结果截图和码云链接;
2.完成实验、撰写实验报告,实验报告以博客方式发表在博客园,注意实验报告重点是运行结果,遇到的问题(工具查找,安装,使用,程序的编辑,调试,运行等)、解决办法(空洞的方法如“查网络”、“问同学”、“看书”等一律得0分)以及分析(从中可以得到什么启示,有什么收获,教训等)。报告可以参考范飞龙老师的指导;
3.严禁抄袭,有该行为者实验成绩归零,并附加其他惩罚措施;
实验步骤:
1.网络编程与安全-1:
① 参考(http://www.cnblogs.com/rocedu/p/6766748.html#SECDSA)
② 结对实现中缀表达式转后缀表达式的功能 MyBC.java
③ 结对实现从上面功能中获取的表达式中实现后缀表达式求值的功能,调用MyDC.java2.网络编程与安全-2:
① 基于Java Socket实现客户端/服务器功能,传输方式用TCP
② 客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式通过网络发送给服务器
③ 服务器接收到后缀表达式,调用MyDC.java的功能计算后缀表达式的值,把结果发送给客户端
④ 客户端显示服务器发送过来的结果3.网络编程与安全-3:
① 基于Java Socket实现客户端/服务器功能,传输方式用TCP
② 客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式用3DES或AES算法加密后通过网络把密文发送给服务器
③ 服务器接收到后缀表达式表达式后,进行解密(和客户端协商密钥,可以用数组保存),然后调用MyDC.java的功能计算后缀表达式的值,把结果发送给客户端
④ 客户端显示服务器发送过来的结果4.网络编程与安全-4:
① 基于Java Socket实现客户端/服务器功能,传输方式用TCP
② 客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式用3DES或AES算法加密通过网络把密文发送给服务器
③ 客户端和服务器用DH算法进行3DES或AES算法的密钥交换
④ 服务器接收到后缀表达式表达式后,进行解密,然后调用MyDC.java的功能计算后缀表达式的值,把结果发送给客户端
⑤ 客户端显示服务器发送过来的结果5.网络编程与安全-5:
① 基于Java Socket实现客户端/服务器功能,传输方式用TCP
② 客户端让用户输入中缀表达式,然后把中缀表达式调用MyBC.java的功能转化为后缀表达式,把后缀表达式用3DES或AES算法加密通过网络把密文和明文的MD5值发送给服务器
③ 客户端和服务器用DH算法进行3DES或AES算法的密钥交换
④ 服务器接收到后缀表达式表达式后,进行解密,解密后计算明文的MD5值,和客户端传来的MD5进行比较,一致则调用MyDC.java的功能计算后缀表达式的值,把结果发送给客户端
⑤ 客户端显示服务器发送过来的结果6.网络编程与安全-6(选做):
Android 开发:客户端功能用Android实现(未完成)7.实验报告中统计自己的PSP(Personal Software Process)时间
前期准备:
1.分工:我负责客户端,张旭升(20162329)负责服务器。
2.清楚自己的IP地址以及可以使用的端口,便于连接时使用:(选用时应选用未被占用的端口)
① 虚拟机Linux下获取网络接口配置信息的命令:ifconfig
查看所有的进程和端口使用情况的命令:netstat –apn
② 在终端命令行下获取网络接口配置信息的命令:
ipconfig
查看TCP和UDP连接的端口号及状态的命令:netstat -a -n
在命令行下和Linux环境下
netstat
命令的使用方法基本相同,常见参数如下:-a
(all)显示所有选项,默认不显示LISTEN(监听)
相关-t
(TCP)仅显示TCP相关选项-u
(UDP)仅显示UDP相关选项-n
拒绝显示别名,能显示数字的全部转化成数字-l
仅列出有在LISTEN
的服务状态-p
显示建立相关链接的程序名-r
显示路由信息,路由表-e
显示扩展信息,例如uid等-s
按各个协议进行统计-c
每隔一个固定时间,执行该netstat
命令-o
表示显示活动的TCP连接并包括每个连接的进程ID(PID)-an
查看所有开放的端口
提示:LISTEN
和LISTENING
的状态只有用-a
或者-l
才能看到
需求分析:
1.需要掌握Socket程序的编写;
2.需要学会如何使用指定算法进行加密和解密;
3.需要掌握给密钥安全传输的基本思路;
4.需要了解对称加密和非对称加密的优点;
代码实现及解释:
关键要点说明:
网络编程中两个主要的问题:
一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何进行可靠高效的数据传输。
对于第一个问题,需要了解TCP(Transmission Control Protocol 传输控制协议)的相关内容:在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以唯一地确定Internet上的一台主机。
对于第二个问题,就可以采用客户端/服务器(C/S)的方式:即通信双方的一方作为服务器等待客户提出请求并予以响应。客户则在需要服务时向服务器提出申请。服务器一般作为守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服务端口,使后来的客户也能及时得到服务。关于Socket方法的简单介绍:
调用Socket方法,可以创建一个流套接字并将其连接到指定主机上的指定端口号。Socket通常用来实现客户方和服务方的连接,连接之后便可实现数据交换。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。Server端
和Client端
都可以通过Send,Write等方法与对方通信。其大致步骤如下:
(1) 创建Socket;
(2) 打开连接到Socket的输入流/输出流;
(3) 按照一定的协议对Socket进行读/写操作;
(4) 关闭Socket.作为客户端的任务:
1.根据指定的、统一的IP地址和端口(未被占用)建立网络连接,为数据交换做准备;
2.数据交换,实现中缀转后缀、密文传输、密钥传输等内容;
3.关闭通信连接,从而释放占用的端口和内存。
测试代码(关键代码及解释)
1.网络编程与安全-1
对于实现后缀表达式求值的功能,只需调用之前已完成的MyDC和相应的测试类测试即可,
其中,使用栈对后缀表达式扫描是一个关键点,思路如下:
① 设置一个操作数栈,开始栈为空;
② 从左到右扫描后缀表达式,遇操作数,进栈;
③ 若遇运算符,则从栈中退出两个元素,先退出的放到运算符的右边,后退出的放到运算符左边,运算后的结果再进栈,直到后缀表达式扫描完毕。
关键代码如下:
public int evaluate (String expr)
{
int op1, op2, result = 0;
String token;
StringTokenizer tokenizer = new StringTokenizer (expr);
while (tokenizer.hasMoreTokens())
{
token = tokenizer.nextToken();
//如果是运算符,调用isOperator
if (isOperator(token))
{
op2 = (stack.pop()).intValue();
//从栈中弹出操作数2
op1 = (stack.pop()).intValue();
//从栈中弹出操作数1
result = evalSingleOp(token.charAt(0),op1,op2);
//根据运算符和两个操作数调用evalSingleOp计算result;
stack.push(new Integer(result));
//计算result入栈;
}
else//如果是操作数
stack.push(new Integer(Integer.parseInt(token)));
//操作数入栈;
}
return result;
}
- 对于实现中缀表达式转后缀表达式的功能MyBC,可以直接在之前完成的四则运算项目中调用相关类并添加一个main方法测试即可,
其中,参考思路如下:
① 设立一个栈,存放运算符,首先栈为空;
② 从左到右扫描中缀式,若遇到操作数,直接输出,并输出一个空格作为两个操作数的分隔符;
③ 若遇到运算符,则与栈顶比较,比栈顶级别高则进栈,否则退出栈顶元素并输出,然后输出一个空格作分隔符;
④ 若遇到左括号,进栈;若遇到右括号,则一直退栈输出,直到退到左括号止;
⑤ 当栈变成空时,输出的结果即为后缀表达式。
关键代码如下:
while (tokenizer.hasMoreTokens()) {
token = tokenizer.nextToken();
if (token.equals("("))
stack1.push(token);
else if (token.equals("+") || token.equals("-")) {
while (!stack1.empty()){
if(stack1.peek().equals("(")){
break;
}else list1.add(stack1.pop());
}
stack1.push(token);
}else if (token.equals("*") || token.equals("/")) {
if(!stack1.empty()) {
if (stack1.peek().equals("*") || stack1.peek().equals("/")) {
list1.add(stack1.pop());
stack1.push(token);
} else stack1.push(token);
}else stack1.push(token);
}
else if (token.equals(")")) {
while (!stack1.peek().equals("(")) {
list1.add(stack1.pop());
}
stack1.pop();
}else list1.add(token);
}
while (!stack1.empty()) {
list1.add(stack1.pop());
}
ListIterator<String > li = list1.listIterator();
while (li.hasNext()) {
Message += li.next() + " ";
li.remove();
}
message = Message;
最后的运行结果如下:
2.网络编程与安全-2
关于使用Java Socket实现客户端/服务器功能(建立连接),可参考一下示例代码(客户端):
import java.io.*;
import java.net.*;
public class TalkClient {
public static void main(String args[]) {
try {
Socket socket = new Socket("127.0.0.1", 4700);
//向本机的4700端口发出客户请求
BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));
//由系统标准输入设备构造BufferedReader对象
PrintWriter os = new PrintWriter(socket.getOutputStream());
//由Socket对象得到输出流,并构造PrintWriter对象
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket对象得到输入流,并构造相应的BufferedReader对象
String readline;
readline = sin.readLine(); //从系统标准输入读入一字符串
while (!readline.equals("bye")) {
//若从标准输入读入的字符串为 "bye"则停止循环
os.println(readline);
//将从系统标准输入读入的字符串输出到Server
os.flush();
//刷新输出流,使Server马上收到该字符串
System.out.println("Client:" + readline);
//在系统标准输出上打印读入的字符串
System.out.println("Server:" + is.readLine());
//从Server读入一字符串,并打印到标准输出上
readline = sin.readLine(); //从系统标准输入读入一字符串
} //继续循环
os.close(); //关闭Socket输出流
is.close(); //关闭Socket输入流
socket.close(); //关闭Socket
} catch (Exception e) {
System.out.println("Error" + e); //出错,则打印出错信息
}
}
}
之后只需创建一个MyBC中的对象并且调用相关方法即可实现数据(后缀表达式)传输功能。
运行结果如下:
我们发现在连续进行数据传输时,传输的后缀表达式有叠加的情况,于是发现是调用MyBC的结果变量message所在的类出现问题,我们单步调试并修改之后便可正常运行:
3.网络编程与安全-3
与2相比,增加了“把后缀表达式用3DES或AES算法加密后通过网络把密文发送给服务器”和“服务器接收到后缀表达式表达式后,进行解密”的内容,我们最初使用的是RSA算法加密,主要运用到密码学相关内容,RSA算法主要代码如下:
创建公钥和私钥:
(1) 创建密钥对生成器:KeyPairGenerator kpg=KeyPairGenerator.getInstance("RSA")
getInstance( )方法的参数是一个字符串,指定非对称加密所使用的算法
(2) 初始化密钥生成器:kpg.initialize(1024)
指定密钥长度。对于RSA算法,密钥长度在512到2048之间。
(3) 生成密钥对:KeyPair kp=kpg.genKeyPair( )
使用KeyPairGenerator类的genKeyPair( )方法生成密钥对,其中包含了一对公钥和私钥的信息。
(4) 获取公钥和私钥:PublicKey pbkey=kp.getPublic( )``PrivateKey prkey=kp.getPrivate( )
在开始处再加上使用IO字符流创建要传输的文件,代码为:
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
ObjectInputStream b=new ObjectInputStream(new FileInputStream("zxs_RSA_pub.txt"));
ObjectInputStream B=new ObjectInputStream(new FileInputStream("lwk_RSA_priv.txt"));
- 对于RSA加密过程,关键点是由于密钥很长,对应的整数值非常大,无法使用一般的整型来存储,所以要使用BigInteger类来存储这类很大的整数,即可进行各种运算。
获取公钥的参数(e, n):
BigInteger e=pbk.getPublicExponent();
BigInteger n=pbk.getModulus();
使用整数表达要加密的明文(字符串),先使用字符串的getBytes( )方法将其转换为byte类型数组,它其实是字符串中各个字符的二进制表达方式,这一串二进制数转换为一个整数非常大,所以要继续使用BigInteger类将这个二进制串转换为整型。
byte ptext[]=s.getBytes("UTF8");
BigInteger m=new BigInteger(ptext);
执行计算:BigInteger c=m.modPow(e,n)
该方法返回的结果即公式me mod n的计算结果,即密文。
完整代码有些乱,就不贴出来了,需要注意各个加密语句的添加位置,运行结果如下:
4.网络编程与安全-4
与3相比,又增加了“客户端和服务器用DH算法进行3DES或AES算法的密钥交换”的内容,我们只使用了RSA算法,这方面需要理解为什么这么做,是为了保证密文安全传输,并且用到了“使用非对称加密来分发对称密钥”的思路。我查了一下3DES和AES算法,之后仅在客户端尝试了DES加密算法。
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream b=new ObjectInputStream(f);
Key k=(Key)b.readObject( ); //从文件中获取密钥
Cipher cp=Cipher.getInstance("DESede"); //创建密码器(Cipher对象)
cp.init(Cipher.ENCRYPT_MODE, k); //初始化密码器
byte ptext[]=s.getBytes("UTF8"); //获取等待加密的明文
for(int i=0;i<ptext.length;i++){
message += citext[i];
}
System.out.println("使用DES加密信息:"+ message);
byte cutext[]=cp.doFinal(pitext); //执行加密
String me = "";
for(int i=0;i<cutext.length;i++){
me += cutext[i] + ",";
}
FileOutputStream f2=new FileOutputStream("lwk.dat");
f2.write(cutext); //处理加密结果
完整代码参考:此链接
关于3DES算法,与DES,AES基本相同,参考代码如下:
//初始化密钥
public static byte[] initKey() throws Exception{
KeyGenerator keyGen = KeyGenerator.getInstance("DESede");
keyGen.init(168);
SecretKey secretKey = keyGen.generateKey();
return secretKey.getEncoded();
}
//加密
public static byte[] encryptTripeDES(byte[] data,byte[] key) throws Exception{
SecretKey secretKey = new SecretKeySpec(key, "DESede");
Cipher cipher = Cipher.getInstance("DESede");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] resultBytes = cipher.doFinal(data);
return resultBytes;
}
//解密
public static byte[] decryptTripeDES(byte[] src,byte[] key) throws Exception{
SecretKey secretKey = new SecretKeySpec(key, "DESede");
Cipher cipher = Cipher.getInstance("DESede");
cipher.init(Cipher.DECRYPT_MODE,secretKey);
byte[] resultBytes = cipher.doFinal(src);
return resultBytes;
}
}
之前使用RSA加密的运行结果如下:
5.网络编程与安全-5
在原来的基础上,又增加了“客户端把密文和明文的MD5值发送给服务器,服务器计算明文的MD5值,和客户端传来的MD5进行比较”的内容,关于MD5非对称加密算法关键代码如下:
MessageDigest mi = MessageDigest.getInstance("MD5"); //使用静态方法getInstance( )生成MessageDigest对象
mi.update(A.getBytes("UTF8" )); //传入需要计算的字符串,update传入的参数是字节类型或字节类型数组
byte si[ ]=mi.digest( ); //执行MessageDigest对象的digest( )方法完成计算
String result="";
for (int i=0; i<si.length; i++){
result+=Integer.toHexString((0x000000ff & si[i]) | 0xffffff00).substring(6);
}
//处理计算结果
os.println(result);
os.flush();
System.out.println("客户端发送哈希值:"+ result);
运行结果如下图:
测试过程及遇到的问题:
1.如何在IPv4地址不能获取的时候实现网络通信和数据传输?
解决办法:(搜索)
通过搜索我了解到127.0.0.1是本地默认的IP地址,一般用来测试。回送地址(127.x.x.x)是本机回送地址,即主机IP堆栈内部的IP地址,主要用于网络软件测试以及本地机进程间通信,即自己跟自己通信。使用ping 127.0.0.1
命令还能测试本机TCP/IP是否正常。得到下图,证实可以使用。2.在使用ipconfig命令时显示:ipconfig不是内部或外部命令。
解决办法:(搜索)
只需要在此电脑-->右键属性-->高级系统设置-->高级-->环境变量-->系统变量,找到"PATH"一项,按“编辑文本”,变量值前面的jdk路径不动,在后面输入英文分号和系统安装路径即可,示例:“;C:\windows\system32”,之后重启命令行,运行成功。3.TCP与UDP传输有什么不同?
解决办法:(搜索)
在做实验时,我对TCP传输产生了疑问,通过搜索,我了解到TCP(Tranfer Control Protocol)提供的是面向连接的、可靠的数据流传输。在任何数据传输前就建立好了点到点的连接。而UDP(User Datagram Protocol)是不可靠的连接,无连接的协议,它在网络上能够以任何可能的路径传往目的地。所以当一个UDP数据包在网络中移动时,发送过程并不知道它是否到达了目的地。但是TCP传输对数据内容正确性的检验占用了计算机的处理时间和网络的带宽,所以TCP传输的效率不如UDP高。简单地说:
TCP类似于打电话,在通讯的过程中连接是一直存在的,电话线断了就不能通讯了。
UDP类似于写信,没有通讯的连接,寄出去一封信,不知道对方是否收到,何时收到。
TCP因有连接的开销,保证了通讯质量,通讯效率较低。
UDP不用建立连接,直接发送,通讯效率较高。说明:本次在实验的过程中还有一些小问题,不过基本上都可以通过单步调试的方法解决(例如第2个实验中调用MyBC类的message值的修复),由于没有及时截图,在这里就不详细叙述了。
分析总结:
本周的实验难度较大,耗时较久。之所以难度较大是因为综合了之前学习的内容,包括IO流、密码学算法、调用数据结构算法等比较全面的内容,而这次还学习到了如何建立服务器和客户端之间的连接、如何实现明文的加密与传输等,也大致了解了java.net.ServerSocket类中的方法。同时,这次实践也让我体会到结对编程的重要性。
感觉这次实验如果是一个人做,就会有很大的工作量,而采用结对的方式,就会高效很多。这次从实验2开始,驾驶员张旭升就率先学习了相关资料,之前我还跟不上他的节奏,后来慢慢的我也学到了很多。对于基础的语法我还有些遗忘,同时也感觉在密码学这部分还是有些乱,不过动手多熟悉几遍代码,感觉就不是那么焦虑了。
本次实验整体上我们进度刚刚好(其实是我在开始时拖延了张旭升的进度),总的来说比较充实,这种状态在下学期要继续保持。
PSP(Personal Software Process)时间统计:
-
步骤 耗时 百分比 需求分析 40min 10% 设计 40min 10% 代码实现 120min 30% 测试 100min 25% 分析总结 100min 25%