我们要通信就必须要有一个服务器和多个客户端,如同打电话时的被叫与主叫,而实现二者的连接就必 须有一套规则,即通信协议。
今天说的就是自定义通信协议来实现文件的传输。
首先我们说一下消息传输时服务器读取消息的规则:
1.读取消息的总长,int型数据
2.读取消息的类型,byte型数据,1为文本聊天消息,2为文件
3.若类型为1时,则再读取接收者的号码(int型数据),消息内容
4.若类型为2时,则读取消息的规则为:读取接收者的号码,读取文件名,读取文件内容,保存文件
具体代码:
服务器:
import java.io.DataInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class Sever { //启动主函数 public static void main(String[] args) { Sever cs = new Sever(); cs.setUpServer(1111); } /** * 在指定端口上启动一个服务器 * @param port:服务器所用的端口 */ public void setUpServer(int port) { try { // 1.建立绑定在指定端口上的服务器对象 ServerSocket server = new ServerSocket(port); while (true) { // 让服务器进处循环等待状态 Socket client = server.accept(); System.out.println("Incoming clieng:"+client.getRemoteSocketAddress()); // 调用处理连接对象的方法去处理连接 processChat(client); } } catch (Exception ef) { ef.printStackTrace(); } } // 处理客户机进入的连接对象 private void processChat(java.net.Socket client) { try { // 得到一个输出/输入流对象 OutputStream out = client.getOutputStream(); InputStream ins = client.getInputStream(); //将输入流包装为DataInputStream方便读取原始类型的流 DataInputStream dins=new DataInputStream(ins); while(true){ //开始读取数据:每一条消息,总是以一个int开头 //1.读消息长度,readInt()方法底层从流中读取4个字节,组成一个int int totalLen=dins.readInt(); System.out.println("*******进入一条消息总长: "+totalLen); //2.读取消息类型标识,只读取一个字节 byte flag=dins.readByte(); System.out.println("消息接收类型为: "+flag); //3.读取目标客户号码,一个int int destNum=dins.readInt(); //消息头解析完毕 System.out.println("消息接收目标用户号是: "+destNum); //根据消息的类型,读取消息体部分 if(flag==1){ //类型为1,是文本聊天消息,按其规则读取 //创建对应消息体部分字节的长度的数据 byte[] data=new byte[totalLen-4-1-4]; //从流中读取data.length个字节放入数组中 dins.readFully(data); String msg=new String(data);//转成字符串 System.out.println("发给文本给:"+destNum+" 内容是:"+msg); } else if(flag==2){ //文件数据包体解析 System.out.println("发送文件给:"+destNum); byte[] data=new byte[256]; dins.readFully(data);//读取256个字节做文件名字 //解析出了文件名字,并去除末尾空格 String fileName=new String(data).trim(); System.out.println("读到的文件名字是: "+fileName); //余下的字节就是文件内容 data=new byte[totalLen-4-1-4-256];//文件字节数据总长 dins.readFully(data);//读入文件的字节 //保存文件到当前目录下: FileOutputStream fous=new FileOutputStream(fileName); fous.write(data); fous.flush(); fous.close(); System.out.println("文件保存完成!"); }else{ System.out.println("收到未知数据包: "+flag); client.close(); } } } catch (Exception ef) { ef.printStackTrace(); } } }
注意:
1.将InputStream包装为DataInputStream流对象:
可以将Socket上得到的输入流包装为DataInputStream流对象,随后,如果调用dins的readByte() 时,只会从底层的数据流中读取一个字节返回;而调用readInt()时,则从底层读取4个字节(32位), readInt() 方法内部经过位运算实现了将读到的4个字节组成一个int型数据返回
2.read()方法与readFully()方法的区别:
当发送大数据量时,有可能一部分数据已发送到对方,有一部分数据还在本地的网卡缓存中,如果调
用read()方法 可能会提前返回而没有读到足够的数据,在传大块数据(如一次传送一个较大文件时)可
能出错,而readfully()方法一直等待,读取到数组长度的所有数据,才会返回。
客户端 :
客户端代码要发送数据时,也必须按照协议格式和顺序将组成消息的各部分数据发送。发送据时应用到 DataOutputStream对象,调用其writeInt(1)方法时,这个整数1会被作为4个字节写入到流中;但如果调用 writeByte(1)方法写入,就只会写入一个字节。
import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import java.util.Scanner; public class Chat { private DataOutputStream dous;//输出流对象 private void writeString(DataOutputStream out,String str,int len){ try{ byte[] data=str.getBytes(); out.write(data); //假设都是短,需要补0 while(len>data.length){ out.writeByte('\0'); len--; } }catch(Exception ef){ ef.printStackTrace(); } } /** * 发送一条文本消息 * @param msg:消息内容 * @param destNum:接收者号码 */ private void sendTextMsg(String msg,int destNum){ try{ byte[] strb=msg.getBytes();//得到消息的字节数 int totalLen=4+1+4+strb.length; System.out.println("发送总长度为: "+totalLen); dous.writeInt(totalLen); //总长 dous.writeByte(1); //类型:1为文本消息 dous.writeInt(destNum);//写入目标用户号 dous.write(strb);//写入消息内容 dous.flush(); }catch(Exception ef){ ef.printStackTrace(); } } /** * 发送一个文件数据包 * @param fileName:文件绝对路径名 * @param destNum:目标用户号码 */ private void sendFileMsg(String fileName,int destNum){ try{ //根据文件名创建文件对象 File file=new File(fileName); //根据文件对象,构造一个输入流 InputStream ins=new FileInputStream(file); int fileDataLen=ins.available();//文件数据总长 int totalLen=4+1+4+256+fileDataLen; //得到了要发送数据包的总长 dous.writeInt(totalLen); dous.writeByte(2);//类型是2,即文件数据包 //写入目标用户号 dous.writeInt(destNum); //文件名 String shortFileName=file.getName(); //写入文件名,不足256个长度时,补\0 writeString(dous,shortFileName,256); byte[] fileData=new byte[fileDataLen]; ins.read(fileData);//读入文件数据 dous.write(fileData);//写出到服务器的流中 dous.flush(); // }catch(Exception ef){ ef.printStackTrace(); } } /** * 连接上服务器 * @param ip:服务器ip * @param port:服务器端口 */ public void conn2Server(String ip, int port) { // 创建一个到服务器端的Socket对象 try { java.net.Socket client = new java.net.Socket(ip, port); // 得到输入输出流对象 InputStream ins = client.getInputStream(); OutputStream ous = client.getOutputStream(); //将输出流包装为DataOutputStream对象 int testCount=0; while(true){ dous=new DataOutputStream(ous); System.out.println("登录服务器成功,请选择你要发的类型(1:聊天 2:文件:"); Scanner sc=new Scanner(System.in); int type=sc.nextInt(); if(type==1){ sendTextMsg("abc聊天内容"+testCount,1111); } if(type==2){ //发文件,注这个文件必须存在 sendFileMsg("G:\\l.txt",1111); } testCount++; } } catch (Exception ef) { ef.printStackTrace(); } } //主函数: public static void main(String[] args) { Chat qqc = new Chat(); qqc.conn2Server("127.0.0.1", 1111); } }