我们要通信就必须要有一个服务器和多个客户端,如同打电话时的被叫与主叫,而实现二者的连接就必 须有一套规则,即通信协议。
今天说的就是自定义通信协议来实现文件的传输。
首先我们说一下消息传输时服务器读取消息的规则:
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);
}
}