干程序是一件枯燥重复的事,每当感到内心浮躁的时候,我就会找小说来看。我从小就喜爱看武侠小说,一直有着武侠梦。从金庸,古龙,梁羽生系列到凤歌(昆仑),孙晓(英雄志)以及萧鼎的(诛仙)让我领略着不一样的江湖。
如果你有好看的武侠系列小说,给我留言哦。题外话就扯这么多了,接着还是上技术。
看看今天实现的功能效果图:
可以这里使用多台手机进行通讯,【凤歌】我采用的服务器发送消息。
是不是只有发送消息,有些显得太单调了。好,在发送消息的基础上增加文件传输。后期会增加视频,音频的传输,增加表情包。那一起来看看图文消息的效果图,带领大家一起来实现通讯的简易聊天功能。
需要解决的难点:
- 如何判断
socket
接收的数据是字符串还是流?
如果你已是一名老司机,还请留言给出宝贵意见。带着这个疑问我们接着往下看。
Socket概述
Socket
我们称之为”套接字”,用于消息通知系统(如:激光推送),时事通讯系统(如:环信)等等。用于描述IP地址
和端口,是一个通信链的句柄。网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket
,一个Socket
由一个IP地址
和一个端口号唯一确定(如:ServerSocket)。应用程序通常通过”套接字”向网络发出请求或者应答网络请求。Socket
是TCP/IP
协议的一个十分流行的编程界面,但是,Socket
所支持的协议种类也不光TCP/IP
一种,因此两者之间是没有必然联系的。在Java环境下,Socket
编程主要是指基于TCP/IP
协议的网络编程。
java.NET
包下有两个类:Socket
和ServerSocket
,基于TCP
协议。
本文针对Socket
和ServerSocket
作主要讲解。
socket连接
建立Socket
连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket
,另一个运行于服务器端,称为ServerSocket
。
客户端Socket
Socket的构造方法:
Socket(InetAddress address,int port); //创建一个流套接字并将其连接到指定 IP 地址的指定端口号
Socket(String host,int port); //创建一个流套接字并将其连接到指定主机上的指定端口号
Socket(InetAddress address,int port, InetAddress localAddr,int localPort); //创建一个套接字并将其连接到指定远程地址上的指定远程端口
Socket(String host,int port, InetAddress localAddr,int localPort); //创建一个套接字并将其连接到指定远程主机上的指定远程端口
Socket(SocketImpl impl); //使用用户指定的 SocketImpl 创建一个未连接 Socket
参数含义:
-
address 双向连接中另一方的IP地址
-
port 端口号
-
localPort 本地主机端口号
-
localAddr 本地机器地址
-
impl 是socket的父类,既可以用来创建serverSocket又可以用来创建Socket
注意:我们在选取端口号的时候需要特别注意,每一个端口提供一种特定的服务,只有给出正确的端口,才能获得相应的服务。0~1023
的端口号为系统所保留,例如http
服务的端口号为80
,telnet
服务的端口号为21
,ftp
服务的端口号为23
。本文选取的端口号为30003
Socket的几个重要方法:
public InputStream getInputStream(); //方法获得网络连接输入,同时返回一个IutputStream对象实例
public OutputStream getOutputStream(); //方法连接的另一端将得到输入,同时返回一个OutputStream对象实例
public Socket accept(); //用于产生"阻塞",直到接受到一个连接,并且返回一个客户端的Socket对象实例。
注意对流异常的处理。
服务端ServerSocket
ServerSocket的构造方法:
ServerSocket(int port); //创建绑定到特定端口的服务器套接字
ServerSocket(int port,int backlog); //利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号
ServerSocket(int port,int backlog, InetAddress bindAddr); //使用指定的端口、侦听 backlog 和要绑定到的本地 IP地址创建服务器
消息与文件的接收和发送
基础常量
Constants常量类:
//连接ip地址 可以通过以下代码查看当前 IP 地址,记住在网络适配里关掉其他的连接,只保留当前连接
//注意关掉防火墙,关闭杀毒软件
/**
* InetAddress ia = null;
* try {
* ia = ia.getLocalHost();
* <p>
* String localname = ia.getHostName();
* String localip = ia.getHostAddress();
* System.out.println("本机名称是:" + localname);
* System.out.println("本机的ip是 :" + localip);
* } catch (Exception e) {
* // TODO Auto-generated catch block
* e.printStackTrace();
* }
*/
public static final String HOST = "192.168.199.164";
//端口号 避免端口冲突 我这里取30003
public static final int PORT = 30003;
//收到消息
public static final int RECEIVE_MSG = 0;
//发送消息
public static final int SEND_MSG = 1;
//发送文件
public static final int SEND_FILE = 2;
//传输文件
public static final int TRANSFER_FILE = 3;
//传输字符串
public static final int TRANSFER_STR = 4;
//聊天列表 发送消息
public static final int CHAT_SEND = 1;
//聊天列表 接收消息
public static final int CHAT_FROM = 2;
//更新进度
public static final int PROGRESS = 5;
注意:关闭多余的网络适配,关闭防火墙。
定义协议
为了保证接收到的数据类型统一(数据是字符串还是流),需要定义协议。定义协议的方式有很多种:
-
发送一个握手信号。 根据握手信号来确定发送的是字符串还是流
-
定义了Header(头)和Body(实体),头是固定大小的,用来告诉接收者数据的格式、用途、长度等信息,接收者根据Header来接受Body。
-
自定义协议
我这里采用的自定义协议,原理跟前面两种类似。我传输的是JSON
数据,根据字段标识传输的是字符串还是流,接收者根据标识去解析数据即可。
协议的实体类(Transmission):
//文件名称
public String fileName;
//文件长度
public long fileLength;
//传输类型
public int transmissionType;
//传输内容
public String content;
//传输的长度
public long transLength;
//发送还是接受类型 1发送 2接收
public int itemType = 1;
//0 文本 1 图片
public int showType;
根据字段transmissionType
去标识传输(序列化)或接收(反序列化)的类型。传输的过程中始终都是以JSON
的格式存在的。传输文件时需要把流转换成字符串(方式很多种我用的是Base64
加密与解密)。
客户端(ClientThread)
public class ClientThread extends Thread {
PrintWriter mPrintWriter;
BufferedReader mBufferedReader;
Socket mSocket;
Handler mSendHandler;
Handler mWriteHandler;
Gson mGson;
public ClientThread(Handler handler) {
mSendHandler = handler;
mGson = new Gson();
}
@Override
public void run() {
super.run();
try {
//创建socket
mSocket = new Socket(Constants.HOST, Constants.PORT);
//获取到读写对象
mPrintWriter = new PrintWriter(mSocket.getOutputStream());
mBufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
//新开线程读取消息 并发送消息
new Thread() {
@Override
public void run() {
super.run();
String content = null;
try {
while ((content = mBufferedReader.readLine()) != null) {
Transmission trans = mGson.fromJson(content, Transmission.class);
if (trans.transmissionType == Constants.TRANSFER_STR) {
Message msg = new Message();
msg.what = Constants.RECEIVE_MSG;
msg.obj = content;
mSendHandler.sendMessage(msg);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
//当前线程创建 handler
Looper.prepare();
mWriteHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == Constants.SEND_MSG) {
mPrintWriter.write(msg.obj.toString() + "\r\n");
mPrintWriter.flush();
} else if (msg.what == Constants.SEND_FILE) {//传输文件
//定义标记判定是字符串还是文件
sendFile(msg.obj.toString());
}
}
};
Looper.loop();
} catch (IOException e) {
e.printStackTrace();
//出现异常关闭资源
try {
if (mPrintWriter != null) {
mPrintWriter.close();
}
if (mBufferedReader != null) {
mBufferedReader.close();
}
if (mSocket != null) {
mSocket.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
/**
* 文件路径
*
* @param filePath
*/
private void sendFile(String filePath) {
FileInputStream fis = null;
File file = new File(filePath);
try {
mSendHandler.sendEmptyMessage(Constants.PROGRESS);
fis = new FileInputStream(file);
Transmission trans = new Transmission();
trans.transmissionType = Constants.TRANSFER_FILE;
trans.fileName = file.getName();
trans.fileLength = file.length();
trans.transLength = 0;
byte[] bytes = new byte[1024];
int length = 0;
while ((length = fis.read(bytes, 0, bytes.length)) != -1) {
trans.transLength += length;
trans.content = Base64Utils.encode(bytes);
mPrintWriter.write(mGson.toJson(trans) + "\r\n");
mPrintWriter.flush();
Message message = new Message();
message.what = Constants.PROGRESS;
message.obj = 100 * trans.transLength / trans.fileLength;
mSendHandler.sendMessage(message);
}
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
if (fis != null) {
try {
fis.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
mPrintWriter.close();
}
}
在MainActivity
中开启客户端线程:
mClientThread = new ClientThread(mHandler);
mClientThread.start();
服务端(MyServer)
public class MyServer {
//多客户端
public static ArrayList<Socket> sSockets = new ArrayList<Socket>();
public static void main(String[] args) {
//DatagramSocket 基于UDP协议的
ServerSocket serverSocket = null;
try {
//创建服务器的socket对象
serverSocket = new ServerSocket(Constants.PORT);
while (true) {
Socket socket = serverSocket.accept();
sSockets.add(socket);
//开启线程
new Thread(new ServerThread(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 25
ServerThread 类:
public class ServerThread implements Runnable {
Socket mSocket;
BufferedReader mBufferedReader;
Gson mGson;
boolean mCreateFile = true;
public ServerThread(Socket socket) throws IOException {
mGson = new Gson();
mSocket = socket;
mBufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream(), "utf-8"));
//新开线程给客服端发送消息
new Thread() {
@Override
public void run() {
super.run();
sendMessage();
}
}.start();
}
@Override
public void run() {
readMessage();
}
//读取的数据发送给客户端
private void readMessage() {
String content = null;
FileOutputStream fos = null;
try {
while ((content = mBufferedReader.readLine()) != null) {
Transmission trans = mGson.fromJson(content, Transmission.class);
if (trans.transmissionType == Constants.TRANSFER_STR) {
System.out.println("" + content);
for (Iterator<Socket> it = MyServer.sSockets.iterator();
it.hasNext(); ) {
if (it == null) {
break;
}
Socket s = it.next();
try {
PrintWriter printWriter = new PrintWriter(s.getOutputStream());
printWriter.write(content + "\r\n");
printWriter.flush();
} catch (SocketException e) {
e.printStackTrace();
it.remove();
}
}
} else {
long fileLength = trans.fileLength;
long transLength = trans.transLength;
if (mCreateFile) {
mCreateFile = false;
fos = new FileOutputStream(new File("d:/" + trans.fileName));
}
byte[] b = Base64Utils.decode(trans.content.getBytes());
fos.write(b, 0, b.length);
System.out.println("接收文件进度" + 100 * transLength / fileLength + "%...");
if (transLength == fileLength) {
mCreateFile = true;
fos.flush();
fos.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
try {
if (fos != null) {
fos.close();
}
if (mBufferedReader != null) {
mBufferedReader.close();
}
if (mSocket != null) {
mSocket.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
MyServer.sSockets.remove(mSocket);
}
}
//发送消息给连接的客服端
private void sendMessage() {
BufferedReader bufferedReader = null;
try {
while (true) {
bufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("请输入发送的字符串:");
String str = bufferedReader.readLine();
for (Iterator<Socket> it = MyServer.sSockets.iterator();
it.hasNext(); ) {
if (it == null) {
break;
}
Socket s = it.next();
try {
Transmission trans = new Transmission();
trans.itemType = Constants.CHAT_FROM;
trans.transmissionType = Constants.TRANSFER_STR;
trans.content = str;
PrintWriter printWriter = new PrintWriter(s.getOutputStream());
printWriter.write(mGson.toJson(trans) + "\r\n");
printWriter.flush();
} catch (SocketException e) {
e.printStackTrace();
s.close();
it.remove();
}
}
}
} catch (IOException e) {
e.printStackTrace();
try {
if (bufferedReader != null) {
bufferedReader.close();
}
MyServer.sSockets.remove(mSocket);
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
首先运行MyServer
的main
函数,把服务启动起来,然后运行MainActivity
把客户端启动起来。这样就实现了两端通讯。
- 上一篇初谈一Dagger2
我的同类文章
- •初谈一Dagger22017-02-12
- •初谈一Java Annotation2017-02-04
- •自定义View之案列篇(四):颜色选择器2016-11-22
- •Android 6.0 运行时权限封装之路2016-11-01
- •自定义View之案列篇(一):魔方2016-10-18
- •EventBus使用大全2017-02-07
- •Android APK 更新之路2016-12-03
- •自定义View之案列篇(三):仿QQ小红点2016-11-08
- •自定义View之案列篇(二):扇形菜单2016-10-24
- •Android Studio JNI 开发简单案例2016-09-29