Socket(套接字)位于iso模型中的传输层之上,应用层(包括表示层和会话层)之下,它是操作系统向外暴露的一些api,使用socket编程,只需要调用这些接口即可。Socket的实现原理和接口的具体使用就不详细描述,本文主要是对本人在使用socket传输数据时遇到的问题和需要注意的事项进行了分析和总结。主要包括以下这些事项:
- 数据丢失
- 死锁
- 传输文本文档时,编码问题
数据传输加密
通过代码来描述遇到的上述问题和注意事项,首先客户端代码如下:
package com.idt.socket;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
public class SocketClient {
public static void main(String[] args) {
// TODO Auto-generated method stub
Socket socket = null;
DataOutputStream dos = null;
DataInputStream dis = null;
FileInputStream fis = null;
try {
System.out.println("socket客户端执行:");
socket = new Socket("199.66.65.120", 8082);
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
File file = new File("E:\\教程\\socket\\data\\测试数据.jpg");
String fileName = file.getName();
long totalLen = file.length();
System.out.println("发送文件名称:"+fileName);
System.out.println("发送文件大小:"+totalLen);
fis = new FileInputStream(file);
byte[] b = new byte[1024];
int len = 0;
int sendLen = 0;
dos.writeInt(fileName.getBytes("utf-8").length);//文件名称字节长度
dos.write(fileName.getBytes("utf-8"));//文件名字节
dos.writeInt((int)totalLen);//文件内容字节长度
//循环写入文件流
while ((len=(fis.read(b, 0, b.length)))>0) {
dos.write(b, 0, len);
dos.flush();
sendLen +=len;
System.out.println("已发送百分比:"+sendLen*100/totalLen+"%("+sendLen+")");
}
} catch (UnknownHostException e) {
// TODO: handle exception
e.printStackTrace();
} catch(IOException e){
e.printStackTrace();
}finally {
try{
if(socket != null){
socket.close();
}
if(dos != null){
dos.close();
}
if(dis != null){
dis.close();
}
if(fis != null){
fis.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
服务端代码:
package com.idt.demo.socket;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServer {
public static void main(String[] args) {
// TODO Auto-generated method stub
DataOutputStream dos = null;
DataInputStream dis = null;
Socket socket = null;
FileOutputStream fos = null;
try {
System.out.println("执行socket服务端:");
ServerSocket serverSocket = new ServerSocket(8082);
socket = serverSocket.accept();
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
//读取文件名称长度
int fileNameLen = dis.readInt();
byte[] fileNameByte = new byte[fileNameLen];
dis.read(fileNameByte,0,fileNameByte.length);
String fileName = new String(fileNameByte, "utf-8");
System.out.println("接收文件名称:"+fileName);
int fileLen = dis.readInt();
System.out.println("接收文件总长度:"+fileLen);
byte[] fileByte = new byte[fileLen];
int readLen = dis.read(fileByte, 0, fileByte.length);
System.out.println("读取的长度:"+readLen);
fos = new FileOutputStream(new File("E:\\教程\\socket\\receive\\"+fileName));
fos.write(fileByte);
fos.flush();
} catch (IOException e) {
// TODO: handle exception
e.printStackTrace();
}finally{
try {
if(dos != null){
dos.close();
}
if(dis != null){
dis.close();
}
if(socket != null){
socket.close();
}
if(fos !=null){
fos.close();
}
} catch (IOException e2) {
// TODO: handle exception
e2.printStackTrace();
}
}
}
}
客户端执行结果打印:
socket客户端执行:
发送文件名称:测试数据.jpg
发送文件大小:349008
已发送百分比:0%(1024)
已发送百分比:0%(2048)
已发送百分比:0%(3072)
已发送百分比:1%(4096)
已发送百分比:1%(5120)
已发送百分比:1%(6144)
已发送百分比:2%(7168)
已发送百分比:2%(8192)
已发送百分比:2%(9216)
已发送百分比:2%(10240)
已发送百分比:3%(11264)
已发送百分比:3%(12288)
已发送百分比:3%(13312)
已发送百分比:4%(14336)
已发送百分比:4%(15360)
已发送百分比:4%(16384)
已发送百分比:4%(17408)
已发送百分比:5%(18432)
已发送百分比:5%(19456)
已发送百分比:5%(20480)
已发送百分比:6%(21504)
已发送百分比:6%(22528)
已发送百分比:6%(23552)
已发送百分比:7%(24576)
已发送百分比:7%(25600)
已发送百分比:7%(26624)
java.net.SocketException: Connection reset by peer: socket write error
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:109)
at java.net.SocketOutputStream.write(SocketOutputStream.java:153)
at java.io.DataOutputStream.write(DataOutputStream.java:107)
at com.idt.socket.SocketClient.main(SocketClient.java:37)
服务端执行结果:
执行socket服务端:
接收文件名称:测试数据.jpg
接收文件总长度:349008
读取的长度:2048
执行过程就是将“测试数据.jpg”文件,从客户端发送到服务端,文件大小是340K。Socket传输完毕后,服务端接受的的“测试数据.jpg”大小与客户端的大小相同(也是340K),但是文件打不开,说明数据没有传输成功。分析代码,发现文件字节长度是349008,但是在服务端只收到了2048个字节,看下面这句代码:
int readLen = dis.read(fileByte, 0, fileByte.length);
上面这句代码,只是将覆盖了fileByte数组的前2048个元素,后面的元素为初始化值,即0。下面这句代码:
fos.write(fileByte);//
将fileByte字节数组写入到文件中,前2048个字节与客户端文件内容保持一致,后面的字节就完全不一样了。将上面代码替换成:
fos.write(fileByte,0,readLen);//
此时,服务端和客户端文件不在一致,而是变小了。
再分析客户端打印日志,客户端socket 写到26624个字节后,开始报socket write error错误,此处有两个疑问:
1. 为什么客户端不再继续write,而是报socket write error错误?
2. 客户端写了26624个字节,服务端为何只接收了2048个字节?
带着这两个疑问,搜索了相关的文章,发现《asynSocket源码解析之三》这篇文章针对这两个疑问讲的非常透彻。
Socket的写和读实际上是写入各自的写缓冲区,从读缓冲区中获取内容,传输层(iso协议)负责将客户端的写缓冲区的包运输到服务端读缓冲区中。
在java socket源码中有两个方法:
public synchronized int getReceiveBufferSize()//获取发送缓冲区大小
public synchronized int getSendBufferSize() throws SocketException//获取接收缓冲区大小
执行这两个方法,会得出java socket默认的发送缓冲区和接收缓冲区大小均为8192。
从以上分析可知,socket传输过程就是:客户端执行while循环不停地向发送缓冲区写数据,传输层负责将客户端的发送缓冲区中的数据运输到服务器的接收缓冲区中,服务端执行read方法从接收缓冲区获取数据。
分析第一个问题,数据丢失,原因就是服务端只读了一次,剩余的字节没有读取,导出文件传输不完全,客户端报socket write error错误,就是因此客户端发送缓冲区和服务器接收缓冲区均已写满,无法再写入,因此报此错误。
我们通过计算对此问题进行深入分析下,客户端发送了26624个字节,服务端写向文件中写入了2048个字节,假设接收缓冲区和发送缓冲区都已填满,那么字节数是16384,由于文件名称的字节较少,可以忽略不计,客户端发送的字节减去服务端向文件写入的字节,再减去缓冲区的字节个数,还剩下8192个字节,那么这8192个字节去哪里了?仔细阅读那篇文章,里面有一句“但是socket其实可用buffer要比实际设置的大。(这个实际大小和设置大小具体啥关系,还不是太清楚)”,也许就是这个原因吧。
服务端socket需要循环读取,才能完全接收文件,修改服务端代码:
public static void main(String[] args) {
// TODO Auto-generated method stub
DataOutputStream dos = null;
DataInputStream dis = null;
Socket socket = null;
FileOutputStream fos = null;
try {
System.out.println("执行socket服务端:");
ServerSocket serverSocket = new ServerSocket(8082);
socket = serverSocket.accept();
dos = new DataOutputStream(socket.getOutputStream());
dis = new DataInputStream(socket.getInputStream());
//读取文件名称长度
int fileNameLen = dis.readInt();
byte[] fileNameByte = new byte[fileNameLen];
dis.read(fileNameByte,0,fileNameByte.length);
String fileName = new String(fileNameByte, "utf-8");
System.out.println("接收文件名称:"+fileName);
int fileLen = dis.readInt();
System.out.println("接收文件总长度:"+fileLen);
//byte[] fileByte = new byte[fileLen];
//int readLen = dis.read(fileByte, 0, fileByte.length);
fos = new FileOutputStream(new File("E:\\教程\\socket\\receive\\"+fileName));
byte[] b = new byte[1024];
int readLen = 0;
while((readLen = dis.read(b, 0, b.length))!=-1){
fos.write(b,0,readLen);
fos.flush();
}
//System.out.println("读取的长度:"+readLen);
} catch (IOException e) {
// TODO: handle exception
e.printStackTrace();
}finally{
try {
if(dos != null){
dos.close();
}
if(dis != null){
dis.close();
}
if(socket != null){
socket.close();
}
if(fos !=null){
fos.close();
}
} catch (IOException e2) {
// TODO: handle exception
e2.printStackTrace();
}
}
}
再次运行代码,服务端可以正常接收文件,客户端不再报错。但是,服务器接收完文件后,希望给客户端返回一个是否接收成功的标识,修改客户端代码,在while循环后面加上:
String rtnMsg = dis.readUTF();
System.out.println("发送结果:" + rtnMsg);
修改服务端代码,也是在while循环后面加上:
dos.writeUTF("succesful");
执行代码,客户端并没有打印发送成功结果,再次运行服务端,提示jvm端口被占用,说明服务端socket还在运行,socket还没有关闭。什么原因呢?
调试代码发现,在服务端while循环一直在执行,无法向下执行到dos.writeUTF("succesful")
这句代码,所以客户端无法读取任何信息。服务端while循环一直执行的原因是什么呢?肯定是socket的输出流读取不到-1这个标志,也就是客户端没有向服务端发送-1标志,socket在什么情况下发送-1这个标志呢?搜索资料发现这篇文章《s.shutdownOutput();这行代码的牛逼之处》针对这个问题讲的非常好。Socket发送-1标志的两种方法:
1. socket.close();
2. socket.shutdownoutput();
第一种方法会造成socket关闭,在socket传输过程中不能使用。因此使用第二方法是合理的。在客户端while循环后面writeUTF方法之前加上方法二的代码。执行代码,客户端成功打印发送成功日志。
问题解决,还要继续分析下,客户端代码在没加读取服务端返回结果的代码之间,为什么可以正常执行呢?服务端没有出现while循环一直执行呢?结合上面,非常好理解,之前没有添加readUTF方法,客户端执行完while循环后继续向下执行,当执行到socket.close()时,服务端接收到-1标志,结束while循环,向下执行直到程序结束。当在客户端代码中添加readUTF方法后,该方法会一直等待服务端向它发送信息,而服务端没有收到客户端向它发送的-1的标志,就会一直执行while循环,无法向下执行writeUTF方法,即无法向客户端发送信息,这样就造成两边相互等待的结果,即死锁现象。
当使用socket传输文本文件时,一定要注意编码的问题,如果客户端和服务端的编码的编码不一致,就会造成乱码的问题。解决乱码采用如下的代码形式:
DataInputStream dataIS = new DataInputStream(clientSocket.getInputStream());
InputStreamReader inSR = new InputStreamReader(dataIS, "UTF-8");
DataOutputStream dataOS = new DataOutputStream(clientSocket.getOutputStream());
OutputStreamWriter outSW = new OutputStreamWriter(dataOS, "UTF-8");
输入流的编码要与当前系统的编码一致,输出流的编码要与接收方系统编码一致。目前,一般情况下系统编码都是utf-8,无需考虑编码问题,直接使用上述的代码即可。
数据传输必须考虑数据的安全性,数据加密是保证数据完整性的有效手段,java中实现的了若干加密算法,如:
1. Base64 严格地说是,属于编码格式;
2. Md5(message digest algorithm 5,信息摘要算法)
3. Sha(secure hash algorithm,安全散列算法)
4. Hmac(hash message authentication code,散列消息鉴别码)