Android 基于TCP的 Socket 编程实现(结合 okio)

前言

       两个进程如果要进行通讯最基本的一个前提就是能够唯一的标识一个进程,在本地进程通讯中我们可以使用 PID 来唯一标识一个进程,但 PID 只在本地是唯一的,网络中两个进程 PID 冲突几率很大,这时我们就需要通过其他手段来唯一标识网络中的进程了,我们知道 IP 层的 ip 地址可以唯一标示主机,而 TCP 层协议和端口号结合就可以唯一标示主机的一个进程了。

能够唯一标示网络中的进程后,它们就可以利用 Socket 进行通信了,什么是 Socket 呢?我们经常把 Socket 翻译为套接字(为什么翻译成套接字),Socket 是在应用层和传输层之间的一个抽象层,它把 TCP/IP 层复杂的操作抽象为几个简单的接口供应用层调用,从而实现进程在网络中通信。


原理图

相关类

       这里提到的 Socket 为广义上的 Socket 编程,它可以基于 TCP 或者 UDP 实现,Java为 Socket 编程封装了几个重要的类,如下:

Socket (TCP)

      Socket 类实现了一个客户端 Socket,作为两台机器通信的终端,默认采用的传输层协议为 TCP 可靠传输协议。Socket 类除了构造函数返回一个 socket 外,还提供了 connect , getOutputStream, getInputStream 和 close 方法。connect 方法用于请求一个 socket 连接,getOutputStream 用于获得写 socket的输出流,getInputStream 用于获得读 socket 的输入流,close 方法用于关闭一个流。

DatagramSocket (UDP)

       DatagramSocket 类实现了一个发送和接收数据报的 socket,传输层协议使用 UDP,不能保证数据报的可靠传输。DataGramSocket 主要有 send, receive 和 close 三个方法。send 用于发送一个数据报,Java 提供了 DatagramPacket 对象用来表达一个数据报。receive 用于接收一个数据报,调用该方法后,一直阻塞接收到直到数据报或者超时。close 是关闭一个 socket。

ServerSocket

       ServerSocket 类实现了一个服务器 socket,一个服务器 socke t等待客户端网络请求,然后基于这些请求执行操作,并返回给请求者一个结果。ServerSocket 提供了 bind、accept 和 close 三个方法。bind 方法为ServerSocket 绑定一个IP地址和端口,并开始监听该端口。accept 方法为 ServerSocket 接受请求并返回一个 Socket 对象,accept 方法调用后,将一直阻塞直到有请求到达。close 方法关闭一个 ServerSocket 对象。

SocketAddress

       SocketAddress 提供了一个 socket 地址,不关心传输层协议。这是一个虚类,由子类来具体实现功能、绑定传输协议。它提供了一个不可变的对象,被 socket 用来绑定、连接或者返回数值。

InetSocketAddress

       InetSocketAddress 实现了IP地址的 SocketAddress,也就是有 IP 地址和端口号表达 Socket 地址。如果不制定具体的 IP 地址和端口号,那么 IP 地址默认为本机地址,端口号随机选择一个。

DatagramPacket(UDP)

        DatagramSocket 是面向数据报 socket 通信的一个可选通道。数据报通道不是对网络数据报 socket 通信的完全抽象。socket通信的控制由DatagramSocket 对象实现。DatagramPacket 需要与 DatagramSocket 配合使用才能完成基于数据报的 socket 通信。

基于TCP的 Socket

       基于 TCP 的 Socket可以实现客户端—服务器间的双向实时通信。上面提到的 java.NET包中定义的两个类 Socket 和 ServerSocket,分别用来实现双向连接的 client 和 server 端。


通信模型

实现

客户端连接:demo


android端效果

客户端发送:消息给服务端


向服务端发数据

服务端代码:

'''

public class SocketTest {

          private static final int PORT =9999;

          private List mList =newArrayList();

          private ServerSocket server =null;

          private ExecutorService mExecutorService =null;

          private String receiveMsg;

          private String sendMsg;

          public static void main(String[] args) {

                      newSocketTest();

          }

         public Socket Test() {

                   try{

                         server =newServerSocket(PORT);

                         mExecutorService = Executors.newCachedThreadPool();

                         System.out.println("服务器已启动...");

                         Socket client =null;

                         while(true) {

                         client = server.accept();

                         mList.add(client);

                         mExecutorService.execute(new Service(client));

                     }

             }catch(Exception e) {

                 e.printStackTrace();

            }

      }

class Service implements Runnable {

private Socket socket;

private BufferedReader in=null;

private PrintWriter printWriter=null;

public Service(Socket socket) {

this.socket = socket;try{

printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter( socket.getOutputStream(),"UTF-8")),true);

in=new BufferedReader(new InputStreamReader(

socket.getInputStream(),"UTF-8"));

printWriter.println("成功连接服务器"+"(服务器发送)");

System.out.println("成功连接服务器");

}catch(IOException e) {

e.printStackTrace();

}

}

@Override

public void run() {

try{

while(true) {

if((receiveMsg =in.readLine())!=null) {

System.out.println("receiveMsg:"+receiveMsg);

if(receiveMsg.equals("0")) {

System.out.println("客户端请求断开连接");

printWriter.println("服务端断开连接"+"(服务器发送)");

mList.remove(socket);

in.close();

socket.close();

break;

}else{

sendMsg ="我已接收:"+ receiveMsg +"(服务器发送)";

printWriter.println(sendMsg);

}

}

}

}catch(Exception e) {

e.printStackTrace();

}

}

}

}

'''

服务端使用线程池实现多客户端连接,server.accept() 表示等待客户端连接,当有客户端连接时新建一个线程去处理,其中涉及到的方法之前都提到过,不再赘述。

客户端代码:

'''

public class SocketActivity extends AppCompatActivity{

private EditText mEditText;

private TextView mTextView;

private static final String TAG ="TAG";

private static final String HOST ="192.168.23.1";

private static final int PORT =9999;

private PrintWriter printWriter;

private BufferedReader in;

private ExecutorService mExecutorService =null;

private String receiveMsg;

@Override

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);

setContentView(R.layout.activity_socket);

mEditText = (EditText) findViewById(R.id.editText);

mTextView = (TextView) findViewById(R.id.textView);

mExecutorService = Executors.newCachedThreadPool();

}

public void connect(View view) {

mExecutorService.execute(newconnectService());

}

public void send(View view) {

String sendMsg = mEditText.getText().toString();

mExecutorService.execute(newsendService(sendMsg));

}

public void disconnect(View view) {

mExecutorService.execute(newsendService("0"));

}

private class sendService implements Runnable{

privateString msg;

sendService(String msg) {

this.msg = msg;

}

@Override

public void run() {

printWriter.println(this.msg);

}

}

private class connectService implements Runnable{

@Override

public void run() {try{

Socket socket =newSocket(HOST, PORT);

socket.setSoTimeout(60000);

printWriter =newPrintWriter(new BufferedWriter(new OutputStreamWriter(

socket.getOutputStream(),"UTF-8")),true);

in =new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));

receiveMsg();

}catch(Exception e) {

Log.e(TAG, ("connectService:"+ e.getMessage()));

}

}

}

private void receiveMsg() {

try{

while(true) {

if((receiveMsg = in.readLine()) !=null) {

Log.d(TAG,"receiveMsg:"+ receiveMsg);

runOnUiThread(new Runnable() {

@Override

public void run() {

mTextView.setText(receiveMsg +"\n\n"+ mTextView.getText());

}

});

}

}

}catch(IOException e) {

Log.e(TAG,"receiveMsg: ");

e.printStackTrace();

}

}

}

'''

客户端同样使用了线程池进行管理,把连接和发送分割为两个 Runnable 易于调用,当发送 “0” 且服务端收到时关闭连接。

okio 实现

到这里一个简单的 Socket 通信就完成了,其中对于 Socket 的信息流使用的是 java.io,之前学习 okio 时,了解到 okio 可以替代 java.io,okio是一个由square公司开发的开源库,它弥补了Java.io和java.nio的不足,能够更方便快速的读取、存储和处理数据(了解更多请点击Okio源码分析),下面就尝试用 okio 替换 java.io。

直接上代码:

服务端代码:

'''

public class SocketTest {

private static final int PORT =9999;

private List mList =newArrayList();

private ServerSocket server =null;

private ExecutorService mExecutorService =null;

private String receiveMsg;

private String sendMsg;

public static void main(String[] args) {

newSocketTest();

}

public SocketTest() {

try{

server =new ServerSocket(PORT);

mExecutorService = Executors.newCachedThreadPool();

System.out.println("服务器已启动...");

Socket client =null;

while(true) {

client = server.accept();

mList.add(client);

mExecutorService.execute(new Service(client));

}

}catch(Exception e) {

e.printStackTrace();

}

}

class Service implements Runnable {

private Socket socket;

private BufferedSink mSink;

private BufferedSource mSource;

public Service(Socket socket) {

this.socket = socket;

try{

mSink = Okio.buffer(Okio.sink(socket));

mSource = Okio.buffer(Okio.source(socket));

sendMsg="成功连接服务器"+"(服务器发送)";

mSink.writeUtf8(sendMsg+"\n");

mSink.flush();

System.out.println("成功连接服务器");

}catch(IOException e) {

e.printStackTrace();

}

}

@Override

public void run() {

try{

while(true) {

for(String receiveMsg; (receiveMsg = mSource

.readUtf8Line()) !=null;) {

System.out.println("receiveMsg:"+ receiveMsg);

if(receiveMsg.equals("0")) {

System.out.println("客户端请求断开连接");

mSink.writeUtf8("服务端断开连接"+"(服务器发送)");

mSink.flush();

mList.remove(socket);

socket.close();

break;

}else{

sendMsg ="我已接收:"+ receiveMsg +"(服务器发送)";

mSink.writeUtf8(sendMsg+"\n");

mSink.flush();

}

}

}

}catch(Exception e) {

e.printStackTrace();

}

}

}

}

'''


客户端代码:

'''

public class SocketActivity extends AppCompatActivity{

private EditText mEditText;

private TextView mTextView;

private static final String TAG ="TAG";

private static final String HOST ="192.168.23.1";

private static final int PORT =9999;

private BufferedSink mSink;

private BufferedSource mSource;

private ExecutorService mExecutorService =null;

@Override

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);

setContentView(R.layout.activity_socket);

mEditText = (EditText) findViewById(R.id.editText);

mTextView = (TextView) findViewById(R.id.textView);

mExecutorService = Executors.newCachedThreadPool();

}publicvoidconnect(View view) {

mExecutorService.execute(new connectService());

}

public void send(View view) {

String sendMsg = mEditText.getText().toString();

mExecutorService.execute(new sendService(sendMsg));

}

public void disconnect(View view) {

mExecutorService.execute(new sendService("0"));

}

private class sendService implements Runnable{

private String msg;

sendService(String msg) {

this.msg = msg;

}

@Override

public void run() {

try{

mSink.writeUtf8(this.msg+"\n");

mSink.flush();

}catch(IOException e) {

e.printStackTrace();

}

}

}

private class connectService implements Runnable{

@Override

public void run() {

try{

Socket socket =newSocket(HOST, PORT);

mSink = Okio.buffer(Okio.sink(socket));

mSource = Okio.buffer(Okio.source(socket));

receiveMsg();

}catch(Exception e) {

Log.e(TAG, ("connectService:"+ e.getMessage()));

}

}

}

private void receiveMsg() {

try{

while(true) {

for(String receiveMsg; (receiveMsg = mSource.readUtf8Line()) !=null; ) {

Log.d(TAG,"receiveMsg:"+ receiveMsg);finalString finalReceiveMsg = receiveMsg;

runOnUiThread(new Runnable() {

@Override

public void run() {

mTextView.setText(finalReceiveMsg +"\n\n"+ mTextView.getText());

}

});

}

}

}catch(IOException e) {

Log.e(TAG,"receiveMsg: ");

e.printStackTrace();

}

}

}

'''

这里有一个很坑的地方:

mSink.writeUtf8(this.msg+"\n");

mSink.flush();

起初没有加 “\n” 时,调用 flush 方法后消息是无法发送成功的,除非调用 sink.close 方法后才会发送成功,但是我们不能每发送一次就 close 掉,对比 printWriter.println 方法,尝试加上一个换行符,果真发送成功。

总结

android有两种通信方式,一种是常用的基于 HTTP 协议方式,另一种就是基于 TCP/UDP 协议的 Socket 方式。虽然大部分需求都可通过 HTTP 实现,实现起来也较为简单,但某些情景下需要使用 Socket 方式,这时永远不要放弃去使用最佳的工具来解决问题的机会。本文主要通过 Socket 实现了 Android 基于 TCP 协议的通信,后面将 Socket 的输入输出流处理由 java.io 替换为 Okio 实现,虽然说 Okio 弥补了Java.io和 java.nio 的不足,能够更方便快速的读取、存储和处理数据,但是实际性能并没测试过,这里主要是为了复习一下 Okio 的使用,另外就是在Okio源码分析中没有涉及到 Socket 的内容,这里正好填补一下知识漏洞。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容