干程序是一件枯燥重复的事,每当感到内心浮躁的时候,我就会找小说来看。我从小就喜爱看武侠小说,一直有着武侠梦。从金庸,古龙,梁羽生系列到凤歌(昆仑),孙晓(英雄志)以及萧鼎的(诛仙)让我领略着不一样的江湖。
如果你有好看的武侠系列小说,给我留言哦。题外话就扯这么多了,接着还是上技术。
看看今天实现的功能效果图:
可以这里使用多台手机进行通讯,【凤歌】我采用的服务器发送消息。
是不是只有发送消息,有些显得太单调了。好,在发送消息的基础上增加文件传输。后期会增加视频,音频的传输,增加表情包。那一起来看看图文消息的效果图,带领大家一起来实现通讯的简易聊天功能。
需要解决的难点:
如何判断socket
接收的数据是字符串还是流?
如果你已是一名老司机,还请留言给出宝贵意见。带着这个疑问我们接着往下看。
Socket概述
Socket
我们称之为”套接字”,用于消息通知系统(如:激光推送),时事通讯系统(如:环信)等等。用于描述IP地址
和端口,是一个通信链的句柄。网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket
,一个Socket
由一个IP地址
和一个端口号唯一确定(如:ServerSocket
)。应用程序通常通过”套接字”向网络发出请求或者应答网络请求。Socket
是TCP/IP
协议的一个十分流行的编程界面,但是,Socket
所支持的协议种类也不光TCP/IP
一种,因此两者之间是没有必然联系的。在Java环境下,Socket
编程主要是指基于TCP/IP
协议的网络编程。
包下有两个类:
Socket
和ServerSocket
,基于TCP
协议。
本文针对Socket
和ServerSocket
作主要讲解。
socket连接
建立Socket
连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket
,另一个运行于服务器端,称为ServerSocket
。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。步骤如下:
服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求
客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
JDK Socket
在包下有两个类:
Socket
和ServerSocket
。ServerSocket
用于服务器端,Socket
是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket
实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。不管是Socket
还是ServerSocket
它们的工作都是通过SocketImpl
类及其子类完成的。接着了解下Socket
和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地址创建服务器
接着我们一起来看看案例。
发送和接收消息
首先来实现一个简单的案例,服务器端一直监听某个端口,等待客户端连接请求。客户端根据IP
地址和端口号连接服务器端,接着客服端通过控制台向服务端发送消息,服务端接收到消息并且展示出来。下面来看看具体实现的步骤:
ClientSocket客服端:
try {Socket socket = new Socket("173.1.1.121", 30004);//获取控制台输入的内容BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));System.out.print("请输入发送的字符串:");String str = bufferedReader.readLine();//给服务端发送消息PrintWriter printWriter = new PrintWriter(socket.getOutputStream());printWriter.write(str + "\r\n");printWriter.flush();//关闭资源bufferedReader.close();printWriter.close();socket.close();} catch (IOException e) {e.printStackTrace();}
Socket
两个参数分别是IP地址
和端口号
,可以通过以下代码获取IP地址
:
InetAddress ia = null;try {ia = ia.getLocalHost();String localname = ia.getHostName();String localip = ia.getHostAddress();System.out.println("本机名称是:" + localname);System.out.println("本机的ip是 :" + localip);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}
注意:关闭多余的网络适配,只保留当前的网络连接。关闭防火墙,安全软件。不然可能导致连接不上。
ServerSocket服务端:
try {mServerSocket = new .ServerSocket(30004);Socket socket = mServerSocket.accept();BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));String content = null;while ((content=bufferedReader.readLine() )!= null) {System.out.println("接收到客服端发来的消息:" +content);}//关闭连接bufferedReader.close();socket.close();} catch (IOException e) {e.printStackTrace();}
服务端和客服端的端口号必须保持一致。下面我们运行两个java
程序看看效果:
可能有些童鞋还不知道,怎么在AndroidStudio
下面运行java
程序,请看下面截图:
传输文件
Socket
只能通过流去读取消息,传输文件需要解决文章开始提出的问题, 如何判断socket
接收的数据是字符串还是流?
定义协议
为了保证接收到的数据类型统一(数据是字符串还是流),需要定义协议。定义协议的方式有很多种:
发送一个握手信号。 根据握手信号来确定发送的是字符串还是流
定义了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)
客户端发送文件的代码如下:
/*** 文件路径** @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();}}
文章结尾处我会附上源码。
trans.content = Base64Utils.encode(bytes);mPrintWriter.write(mGson.toJson(trans) + "\r\n");mPrintWriter.flush();
把字节流转换成字符串传输Base64Utils.encode(bytes)
,接收方把字符串解析成字节流并写入文件。
注意:在Android程序中运行,记得添加网络文件读写的权限。
服务端(ServerThread)
服务端接收文件的代码如下:
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();}
服务端接收到文件,并存储到了d
盘。注意文件传输结束后关闭流。
源码地址
下载源码后,请先替换Constants
类中HOST
地址,然后运行MyServer
的java
程序,最后运行MainActivity
的Android
程序。
参考文章:
聊聊Socket、TCP/IP、HTTP、FTP及网络编程
MultiType-FilePicker
Android 6.0 运行时权限封装之路