Socket通信

前言

socket是套接字,是一个对 TCP / IP协议进行封装的编程调用接口,嗯。。。比较官方规范的介绍方式,但如果你刚接触网络编程时,看到这个解释可能会想说,谁他妈知道套接字是个什么鬼。感觉就像用原版的英语词典查一个生词,单词的解释是一句英语,有几个词不认识,得递归查,内心神兽奔腾而过~-~。

简单介绍一下socket相关知识点:


1.协议

可以先简单理解为约定,比如usb接口,定义了usb的各种标准,包括它的基本外观,尺寸,和其他什么参数,然后所有需要usb接口的产品都按照约定来做,这样子,随便买根数据线,在哪里都能传输数据,充电,正常工作。那么就可以理解,TCP和UDP作为一种网络运输层的协议,对于两个网络设备,如果都实现了该协议,那么在该协议的基础上,设备之间的网路运输层就可以正常交互。

2.Socket和Http

网络分为5层,最上面两层分别是应用层和运输层,TCP和UDP是实现了运输层的协议,而Socket是对TCP和UDP协议进行了封装,方便上层调用,HTTP属于应用层,他们不在一个层级,本不具备可比性,运输层协议是为了约定数据传输的方式,应用层协议是为了解决数据的包装方式。

举个栗子:

从学校前门到学校后门,我们需要送一千本书过去。

  • 这里书就是要传输的数据,前门和后门相当于需要通信的客户端和服务端。
  • 出于运输速度和人力资源分配的考虑,我们约定书要分成最多100本一捆的格式去运输比较高效,这一千本书要按顺序分成1~100,101~200。。。901~1000,这样子依次送,这就是运输的协议。
    • 那么在学校后门这里,因为我知道从前门送过来的书是每捆100本按顺序依次送过来,那么我把这些书按照收到的顺序依次拼接起来,那么就保证了些书原本的顺序,即数据的完整性,有序性没有被破环。前门运输之前,书是什么样子,后门这里拿到后还是什么样子。
  • 而如果我们约定这些书是用纸箱装还是用车拖,学校的后门的地理位置在哪,送到后门后如果没有被及时领走的话,应该保存多久,到期了是扔掉还是怎么处理。这些除数据内容本身以外的约定,Http协议在解决的问题。
  • 如果一定要把Socket和Http拿来比较,差别就是在工作方式上,Http通信中只有客户端向服务端请求了数据,服务端才能响应并发数据给客户端,而Socket通信中,只要客户端和服务端之间建立了连接,那么在客户端没有请求数据的情况下,服务端可以主动向客户端发送数据。

3.TCP和UDP

其实在网络编程中,TCP的更为常用一些,这里简单聊一下区别
TCP 3次握手举例:

Client:“Service,我要连”
Service: “好,我知道你要连,同意Client连接”
Client:“哦,我知道你知道我要连”

  • TCP:面向连接、面向字节流、双向通信、 可靠
    • 面向连接:TCP在客户端和服务端数据交互之前,有一个3次握手确认连接的过程,只有客户端和服务端都确认了彼此的连接状态,才会开始传输数据。
      • 以上,就可以建立连接通道了,这样的好处是避免网络延时等情况下,Client发送连接请求后,Service没收到,而等到Client已经不需要和Service通信后,Service才收到连接请求,如果直接就这样子建立通道了,会造成资源浪费。
      • 即然说到连接的3次握手,那就有必要提一下TCP断开连接的4次回收,即任一方发送“我要断开连接的通知”,接收方回复“已经知晓你断开连接了”,4次是因为任一方都可以发送断开连接的消息。
    • 面向字节流:流是字符序列。对于TCP而言,传输的报文长度有最大限制,对于更大的数据而言,就必须把该数据分割成一块块的数据块,全部传输完成后,在拼接成原始数据。
    • 可靠:按顺序传输数据、不丢失、不重复
  • UDP:无连接的、面向报文、不可靠
    • 无连接、不可靠:不需要像TCP那样,建立连接后通讯,它只需要数据要送到哪里去,就可以开始传输数据,至于数据是否丢失,一概不管
    • 面向报文:数据有多大,UDP就一次性传输多大,不做切割数据的动作

使用方式:


  • 客户端实现

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_client_tcp);
    ButterKnife.bind(this);
    //线程池
    pools = Executors.newCachedThreadPool();
    //启动服务端
    startService(new Intent(this, ServiceTcp.class));
}

这里我们创建一个Activity命名为ClientTcp左为客户端
方便起见,用线程池替代创建线程执行任务

pools.execute(new Runnable() {
                @Override
                public void run() {
              //在连接成功以前,会循环尝试连接知道成功为止
                    while (mSocket == null || !mSocket.isConnected()) {
                        try {
                          //这里构造socket传入ip和端口号,拿到socket对象
                            mSocket = new Socket("localhost", 2000);
                        } catch (final IOException e) {
                            e.printStackTrace();
                          //toast要切换到主线程中执行
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    Toast.makeText(ClientTcp.this, "连接失败" + "\r\n" + "一秒后重连" + "\r\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
                                }
                            });
                            try {
                                Thread.sleep(1000);     //延时1秒重连
                            } catch (InterruptedException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
                    try {
                    //注意,在上面连接成功以后代码才会走到这里,否则在while循环那里是阻塞的状态
                        br = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
                        os = mSocket.getOutputStream();//初始化输入输出流
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(ClientTcp.this, "连接成功", Toast.LENGTH_SHORT).show();
                        }
                    });
                    //死循环接受消息
                    acceptMessage(br);
                }
            });

在子线程我,我们尝试连接Service段socket,做了连接失败后重新连接的处理以及输入输出流的初始化,接下来,我们看看acceptMessage(br)方法做了在接收数据时做了怎样的处理

private void acceptMessage(BufferedReader br) {
    while (mSocket.isConnected()){
        try {
            while (!br.ready()){} //当流中没有数据的时候,会一直阻塞在这里
            final String response = br.readLine();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //将用以标记换行的符号"~~"还原,并切换到主线程中显示
                    String[] split = response.split("~~");
                    mShowMessage.setText(split[0] + "\n" + split[1]);
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

当socket中获取到的输入流中没有数据时,下面的代码不会执行,一旦有数据,就会调用readLine()方法读出字符,这里做了一个小处理:服务端把接受的详细acceptMessage和要回复的消息responseMessage以acceptMessage + "\n" + responseMessage凭借换行符后发给客户端,但是客户端在执行readLinea()读取数据时,读到"\n"便会停止读取,如果没有读到取"\n",便会一直阻塞 在这里,这也是为什么客户端和服务端在要发送的消息字符串的末尾都会添加"\n"。所以客户端接收到服务端的消息后,将临时定义的“~~”再替换回换行符号,让textview显示的时后能方便区分发送的和接受的消息

case R.id.sendMessage:
            String str = mEt.getText().toString();
            if (os != null && !TextUtils.isEmpty(str)){
                try {
                    //在字符串末尾拼接换行符,避免服务端socket读取数据时阻塞线程
                    os.write((str + "\r\n").getBytes());
                    os.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            break;

发送很简单,需要注意一下flush()和close的区别,close()会关闭掉流,在关闭掉之前会刷新一次流中的数据,那么这个流接下来就不能使用了,而flush刷新后,可以继续使用

  • 服务端实现

@Override
public void onDestroy() {
    super.onDestroy();
    isServideTcpDestory = true;
}
private class TcpAcceptRunnable implements Runnable {
    protected ServerSocket mServerSocket;
    @Override
    public void run() {
        try {
            mServerSocket = new ServerSocket(2000);
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (!isServideTcpDestory) {
            try {
                //这里是一个阻塞方法,如果没有客户端请求连接,线程会停在这里等待
                final Socket socket = mServerSocket.accept();
                mThreadPools.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            responseTcpClient(socket);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端,只要在服务开启时,执行这个Runnable就可以了,在服务ondestory时,会将isServideTcpDestory值设置为true,从而终止while死循环。值得一提的是,new ServiceSocket(2000)会使该线程监听自己的2000端口,这里的mServerSocket.accept()方法会产生一个socket,但知道有客户端请求连接2000端口位置,线程会一直阻塞在这里。拿到和特定客户端对应的服务端Socket对象后,我们看看在子线程中responseTcpClient(socket)方法作了什么处理

 private void responseTcpClient(Socket client) throws IOException {
    //操作流
    BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
    String acceptResponse = "";
    while (!isServideTcpDestory) {
        while (!br.ready()) { }
        acceptResponse = br.readLine();
        String responseStr = mStrings[new Random().nextInt(mStrings.length)];
        //因为readLine()方法在读到换行符之前会一直等待,这里用"~~"代替换行,在clientTcp中拿到数据再替换成换行符设置给textview
        bw.write("sendMessage: " + acceptResponse + "~~" + "receiveResponse: " + responseStr + "\r\n");
        bw.flush();
    }
    br.close();
    bw.close();
    client.close();
}

看起来,和在客户端的acceptMessage(BufferedReader br)方法中并没有什么差别

我们来看看最后的效果

SocketDemoGIF.jpg

如果需要Demo源码,请移步GitHub: SocketDemo

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

推荐阅读更多精彩内容

  • 1)OSI与TCP/IP各层的结构与功能,都有哪些协议。 OSI分层 (7层):物理层、数据链路层、网络层、传输层...
    ldlywt阅读 2,277评论 0 26
  • socket通信原理 socket又被叫做套接字,它就像连接到两端的插座孔一样,通过建立管道,将两个不同的进程之间...
    jiodg45阅读 1,068评论 0 1
  • 前言 我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服...
    Chars阅读 2,892评论 2 109
  • 一、网络各个协议:TCP/IP、SOCKET、HTTP等## 网络七层由下往上分别为物理层、数据链路层、网络层、传...
    Mighty_man阅读 436评论 0 2
  • 本文参考:1. socket 百度百科 2.博主:张明伟 要了解Socket,我们就必须要和http一起来了解,看...
    DXSmile阅读 8,026评论 3 30