课程:Java程序设计实验 班级:1353 姓名:符余佳源 学号:20135321
成绩: 指导教师:娄嘉鹏 实验日期:2015.6.9
实验密级:无 预习程度: 实验时间:15:30~18:00
仪器组次: 21 必修/选修: 选修 实验序号:5
实验名称:TCP传输及加解密
产品托管地址:http://git.shiyanlou.com/20135321/shiyanlou_cs212
实验内容:
1.运行教材上TCP代码,结对进行,一人服务器,一人客户端;
2.利用加解密代码包,编译运行代码,一人加密,一人解密;
3.集成代码,一人加密后通过TCP发送;
注:加密使用AES或者DES/AES或者DES加密密钥key并发送,使用服务器的公钥加密/公钥算法使用RSA或DH/检验发送信息的完整性使用MD5或者SHA3;
4.完成Blog。
实验仪器:
名称 |
型号 |
数量 |
PC |
ACER |
1 |
虚拟机 |
实验楼 |
1 |
一.实验步骤
(一)在进行实验之前,我花出一定的时间了解了AES及DES算法的相关内容,并再将JAVA的一些常用的语句进行了一定的回顾,为本次实验奠定一定的基础。
我(20135321)在本次实验中主要负责服务器端,我的搭档符运锦(20135323)负责客户端,他的博客园地址为:http://www.cnblogs.com/20135323fuyunjin
(二)回顾TCP
服务器端代码:
package chapter9;
import java.io.*;
import java.net.*;
public class ServerTest {
public static final int PORT=8080;
public static void main(String[] args)throws IOException{
ServerSocket s=new ServerSocket(PORT);
System.out.println("Started:"+s);
try{
Socket socket=s.accept();
try{
System.out.println("Connection accepted:"+socket);
BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out=new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
while(true){
String str=in.readLine();
if(str.equals("END"))
break;
System.out.println("Echoing:"+str);
out.println(str);
}
}finally{
System.out.println("closing...");
socket.close();
}
}finally{
s.close();
}
}
}
客户端代码:
package chapter9;
import java.io.*;
import java.net.*;
public class ClientTest {
public static void main(String[] args)throws IOException{
InetAddress addr=InetAddress.getByName(null);
System.out.println("addr="+addr);
Socket socket=new Socket(addr,ServerTest.PORT);
try{
System.out.println("socket="+socket);
BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out=new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
for(int i=0;i<10;i++){
out.println("howdy"+i);
String str=in.readLine();
System.out.println(str);
}
out.println("END");
}finally{
System.out.println("closing...");
socket.close();
}
}
}
注意:要先运行服务器端代码再运行客户端代码
运行结果截图如下:
(三)前期知识准备
什么是客户端?
客户端(Client)或称为用户端,是指与服务器相对应,为客户提供本地服务的程序。除了一些只在本地运行的应用程序之外,一般安装在普通的客户机上,需要与服务端互相配合运行。因特网发展以后,较常用的用户端包括了如万维网使用的网页浏览器,收寄电子邮件时的电子邮件客户端,以及即时通讯的客户端软件等。对于这一类应用程序,需要网络中有相应的服务器和服务程序来提供相应的服务,如数据库服务,电子邮件服务等等,这样在客户机和服务器端,需要建立特定的通信连接,来保证应用程序的正常运行。
原理:
客户端及伺服端的关系不见得一定建立在两台分开的机器上,同一台机器中也有这种主从关系的存在。提供服务的伺服端及接受服务的客户端也有可能都在同一台机器上,例如我们在提供网页的服务器上执行浏览器浏览本机所提供的网页,这样在同一台机器上就同时扮演伺服端及客户端。
什么是服务器端?
服务器端(Server)是指在网络编程中被动等待连接的程序,服务器端一般实现程序的核心逻辑以及数据存储等核心功能。服务器端的编程步骤和客户端不同,是由四个步骤实现,依次是:
①监听端口: 服务器端属于被动等待连接,所以服务器端启动以后,不需要发起连接,而只需要监听本地计算机的某个固定端口即可。这个端口就是服务器端开放给客户端的端口,服务器端程序运行的本地计算机的IP地址就是服务器端程序的IP地址。
②获得连接:当客户端连接到服务器端时,服务器端就可以获得一个连接,这个连接包含客户端的信息,例如客户端IP地址等等,服务器端和客户端也通过该连接进行数据交换。一般在服务器端编程中,当获得连接时,需要开启专门的线程处理该连接,每个连接都由独立的线程实现。
③交换数据:服务器端通过获得的连接进行数据交换。服务器端的数据交换步骤是首先接收客户端发送过来的数据,然后进行逻辑处理,再把处理以后的结果数据发送给客户端。简单来说,就是先接收再发送,这个和客户端的数据交换数序不同。其实,服务器端获得的连接和客户端连接是一样的,只是数据交换的步骤不同。当然,服务器端的数据交换也是可以多次进行的。
④关闭连接:当服务器程序关闭时,需要关闭服务器端,通过关闭服务器端使得服务器监听的端口以及占用的内存可以释放出来,实现了连接的关闭。
什么是TCP?
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内[1] 另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。[1]
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元([1] MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体[1] 的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
在数据正确性与合法性上,TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和;同时可以使用md5认证对数据进行加密。
(四)具体操作
1.运行DES加密代码
import java.io.*;
import javax.crypto.*;
public class Skey_DES{
public static void main(String args[]) throws Exception{
KeyGenerator kg=KeyGenerator.getInstance("DESede");
kg.init(168);
SecretKey k=kg.generateKey( );
FileOutputStream f=new FileOutputStream("key1.dat");
ObjectOutputStream b=new ObjectOutputStream(f);
b.writeObject(k);
}
}
设置加解密文件
import java.io.*;
import java.security.*;
public class Skey_kb{
public static void main(String args[]) throws Exception{
FileInputStream f=new FileInputStream("key1.dat");
ObjectInputStream b=new ObjectInputStream(f);
Key k=(Key)b.readObject( );
byte[ ] kb=k.getEncoded( );
FileOutputStream f2=new FileOutputStream("keykb1.dat");
f2.write(kb);
// 打印密钥编码中的内容
for(int i=0;i<kb.length;i++){
System.out.print(kb[i]+",");
}
}
}
运行截图:
2.原理解释:
(1)throws Exception {}//表示该方法可能产生异常,并使用throws声明异常,//则该方法产生异常也不必捕获
(2)例如想用“hello”作为密钥利用DES进行加密和解密:
String keyString=”hello”;
byte[]keyData=keyString.getBytes();
secretKey myDeskey=new SecretKeySpec(keyData,”DES”);
(3) 获取密钥生成器
KeyGenerator kg=KeyGenerator.getInstance("DESede");
Java中KeyGenerator类中提供了创建对称密钥的方法。Java中的类一般使用new操作符通过构造器创建对象,但KeyGenerator类不是这样,它预定义了一个静态方法getInstance(),通过它获得KeyGenerator类型的对象。这种类成为工厂类或工厂。
方法getInstance( )的参数为字符串类型,指定加密算法的名称。
(4)初始化密钥生成器
kg.init(168);
该步骤一般指定密钥的长度。如果该步骤省略的话,会根据算法自动使用默认的密钥长度。指定长度时,若第一步密钥生成器使用的是“DES”算法,则密钥长度必须是56位;若是“DESede”,则可以是112或168位,其中112位有效;若是“AES”,可以是128, 192或256位。
(5)生成密钥SecretKey k=kg.generateKey( );
使用第一步获得的KeyGenerator类型的对象中generateKey( )方法可以获得密钥。其类型为SecretKey类型,可用于以后的加密和解密。
(6)通过对象序列化方式将密钥保存在文件中
FileOutputStream f=new FileOutputStream("key1.dat");
ObjectOutputStream b=new ObjectOutputStream(f); b.writeObject(k);
ObjectOutputStream类中提供的writeObject方法可以将对象序列化,以流的方式进行处理。这里将文件输出流作为参数传递给ObjectOutputStream类的构造器,这样创建好的密钥将保存在文件key1.dat中。
(五)代码实现
在本次实验中,我们采取了一台计算机进行结对试验。当运用一台计算机进行本次实验室,只需要开出两个CMD窗口即可。
服务器代码(20135321余佳源):
import java.net.*;
import java.io.*;
import java.security.*;
import java.security.spec.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
import java.security.interfaces.*;
import java.math.*;
public class ComputeTCPServer{
public static void main(String srgs[]) throws Exception {
ServerSocket sc = null;//空代表可以使用默认值
Socket socket=null;
try {
sc= new ServerSocket(2123);//创建服务器套接字
System.out.println("端口号:" + sc.getLocalPort());
System.out.println("服务器已经启动...");
socket = sc.accept(); //等待客户端连接
System.out.println("已经建立连接");
//获得网络输入流对象的引用
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
////获得网络输出流对象的引用
PrintWriter out=new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
String aline2=in.readLine();//读取客户端传送来的数据
BigInteger c=new BigInteger(aline2);
FileInputStream f=new FileInputStream("Skey_RSA_priv.dat");//从Skey_RSA_priv.dat中获得输入字节
ObjectInputStream b=new ObjectInputStream(f);
RSAPrivateKey prk=(RSAPrivateKey)b.readObject( );//RSA 专用密钥的接口
BigInteger d=prk.getPrivateExponent();//进行大数运算
BigInteger n=prk.getModulus();
BigInteger m=c.modPow(d,n);
byte[] keykb=m.toByteArray();
String aline=in.readLine();//读取客户端传送来的数据
byte[] ctext=parseHexStr2Byte(aline);
Key k=new SecretKeySpec(keykb,"DESede");//使用SecretKeySpec类来根据一个字节数组构造一个 SecretKey.
//仅对能表示为一个字节数组并且没有任何与之相关联的钥参数的原始密钥有用,如,DES
Cipher cp=Cipher.getInstance("DESede");//加密和解密
cp.init(Cipher.DECRYPT_MODE, k);
byte []ptext=cp.doFinal(ctext);
String p=new String(ptext,"UTF8");
System.out.println("从客户端接收到信息为:"+p); //通过网络输出流返回结果给客户端
String aline3=in.readLine();
String x=p;
MessageDigest m2=MessageDigest.getInstance("MD5");//为应用程序提供信息摘要算法的功能,如 MD5算法
//是单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值。
m2.update(x.getBytes( ));
byte a[ ]=m2.digest( );
String result="";
for (int i=0; i<a.length; i++){
result+=Integer.toHexString((0x000000ff & a[i]) |
0xffffff00).substring(6);
}
System.out.println(result);
if(aline3.equals(result)){
System.out.println("匹配成功");
}
out.println("匹配成功");
out.close();
in.close();
sc.close();
} catch (Exception e) {
System.out.println(e);
}
}
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF);
if (hex.length() == 1) {
hex = ‘0‘ + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length()/2];
for (int i = 0;i< hexStr.length()/2; i++) {
int high = Integer.parseInt(hexStr.substring(i*2, i*2+1 ), 16); //将String字符类型数据转换为Integer整型数据
int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
result[i] = (byte) (high * 16 + low);
}
return result;
}
}
客户端代码(20135323符运锦):
import java.net.*;
import java.io.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.spec.*;
import javax.crypto.interfaces.*;
import java.security.interfaces.*;
import java.math.*;
public class ComputeTCPClient {
public static void main(String srgs[]) throws Exception{
try {
KeyGenerator kg=KeyGenerator.getInstance("DESede");//KeyGenerator类提供(对称)密钥生成器的功能
//密钥生成器是使用此类的某个 getInstance 类方法构造的
//方法getInstance( )的参数为字符串类型,指定加密算法的名称
kg.init(168); //初始化指定密钥的长度
SecretKey k=kg.generateKey( );//秘密(对称)密钥。实现此接口的密钥以其编码格式返回字符串,并返回作为 getEncoded 方法调用结果的原始密钥字节
byte[] ptext2=k.getEncoded();//设置一个字节数组承载原始密钥字节
//创建连接特定服务器的指定端口的Socket对象
Socket socket = new Socket("192.168.1.115", 2123);//该IP地址是寝室网IP
//获得从服务器端来的网络输入流
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获得从客户端向服务器端输出数据的网络输出流
PrintWriter out=new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
//创建键盘输入流,以便客户端从键盘上输入信息
BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
FileInputStream f3=new FileInputStream("Skey_RSA_pub.dat");
ObjectInputStream b2=new ObjectInputStream(f3);
RSAPublicKey pbk=(RSAPublicKey)b2.readObject( );
BigInteger e=pbk.getPublicExponent();
BigInteger n=pbk.getModulus();
BigInteger m=new BigInteger(ptext2);
BigInteger c=m.modPow(e,n);
String cs=c.toString( );
out.println(cs); //通过网络传送到服务器
System.out.print("请输入待发送的数据:");
String s=stdin.readLine(); //从键盘读入待发送的数据
Cipher cp=Cipher.getInstance("DESede");//DES算法的解码过程
cp.init(Cipher.ENCRYPT_MODE, k);//DES算法的解码过程
byte ptext[]=s.getBytes("UTF8");//DES算法的解码过程
byte ctext[]=cp.doFinal(ptext);//DES算法的解码过程
String str=parseByte2HexStr(ctext);//DES算法的解码过程
out.println(str); //通过网络传送到服务器
String x=s;
MessageDigest m2=MessageDigest.getInstance("MD5");//MessageDigest类为应用程序提供信息摘要算法的功能如MD5算法
m2.update(x.getBytes( ));
byte a[ ]=m2.digest( );
String result="";
for (int i=0; i<a.length; i++){
result+=Integer.toHexString((0x000000ff & a[i]) |
0xffffff00).substring(6);////以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式。
}
System.out.println(result);
out.println(result);
str=in.readLine();//从网络输入流读取结果
System.out.println( "从服务器接收到的结果为:"+str); //输出服务器返回的结果
}
catch (Exception e) {
System.out.println(e);
}
finally{
}
}
public static String parseByte2HexStr(byte buf[]) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < buf.length; i++) {
String hex = Integer.toHexString(buf[i] & 0xFF); //返回的字符串表示的无符号整数参数所表示的值以十六进制(基数为16)
if (hex.length() == 1) {
hex = ‘0‘ + hex;
}
sb.append(hex.toUpperCase());
}
return sb.toString();
}
public static byte[] parseHexStr2Byte(String hexStr) {
if (hexStr.length() < 1)
return null;
byte[] result = new byte[hexStr.length()/2];
for (int i = 0;i< hexStr.length()/2; i++) {
int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16); //将String字符类型数据转换为Integer整型数据
int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16); //将String字符类型数据转换为Integer整型数据
result[i] = (byte) (high * 16 + low);
}
return result;
}
}
(六)整合
1.寝室网IP地址:
整合操作截图:
(七)PSP时间
步骤 |
耗时 |
百分比 |
需求分析 |
50min |
16.1% |
设计 |
60min |
19.3% |
代码实现 |
120mn |
38.7% |
测试 |
20min |
6.5% |
分析总结 |
60min |
19.3% |
(八)遇到的问题
1.一开始并没有想着建立两台laptop之间的互用局域网导致不能够实现服务器端和客户端的互联
解决方案:用电脑放出wifi或者手机开出热点或者使用寝室里的路由器进行局域传输。
2.在单独一台laptop上进行模拟实验的时候,一开始没有想到使用两个CMD窗口来操作,单独一个eclipse也不能实现得了服务器端和客户端的联系。
解决方案:经过符运锦的提醒点拨就想到了使用两个CMD窗口,成功实现了模拟互联。
(九)实验体会及感想
这是第四次java实验,也是本学期最后一次的java实验了。相对比于前三次的实验,这次实验更着重的是对代码的整合和搭配,还有在一定程度上考察了我们对密码学知识的理解。同样的这是一次结对编程实验,领航员和驾驶员都很重要,两者也可以互换角色,这一点我深有体会,因为一开始我是觉得我可以胜任很大部分的代码工作,到后来却是符运锦同学把握点拨出了实验的重点。不管如何,我们还是在实验中收获颇丰,组合代码也是一个java程序员应该具备的技能,所以我感到很幸运。