Socket通信

2017-08-30  本文已影响0人  yanlongRivenK

前言

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

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


1.协议

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

2.Socket和Http

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

举个栗子:

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

3.TCP和UDP

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

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

使用方式:


@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

上一篇 下一篇

猜你喜欢

热点阅读