Android笔记:Android中的socket通信

最近在整理socket知识,特意做了一个笔记,以供以后查阅。

1.简介

IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层,socket则是对TCP/IP协议的封装和应用(程序员层面上),它并不是一个协议。

在TCP/IP协议族中主要的Socket类型为流套接字(streamsocket)和数据报套接字(datagramsocket)。流套接字将TCP作为其端对端协议,据报套接字使用UDP协议。

2.通信模型

socket通信模型

3.Android中的源码实现

3.1 TCP模式

Java中基于TCP协议实现网络通信的类:

  • 客户端的Socket类
  • 服务器端的ServerSocket类
tcp socket模型

3.1.1 TCP通信实现步骤

             ① 创建ServerSocket和Socket
             ② 打开连接到Socket的输入/输出流
             ③ 按照协议对Socket进行读/写操作
             ④ 关闭输入输出流、关闭Socket

服务器端:
① 创建ServerSocket对象,绑定监听端口
② 通过accept()方法监听客户端请求
③ 连接建立后,通过输入流读取客户端发送的请求信息
④ 通过输出流向客户端发送响应信息
⑤ 关闭相关资源

/**
 * 基于TCP协议的Socket通信,实现用户登录,服务端
*/
//1、创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket =newServerSocket(10086);//1024-65535的某个端口
//2、调用accept()方法开始监听,等待客户端的连接
Socket socket = serverSocket.accept();
//3、获取输入流,并读取客户端信息
InputStream is = socket.getInputStream();
InputStreamReader isr =newInputStreamReader(is);
BufferedReader br =newBufferedReader(isr);
String info =null;
while((info=br.readLine())!=null){
System.out.println("我是服务器,客户端说:"+info);
}
socket.shutdownInput();//关闭输入流
//4、获取输出流,响应客户端的请求
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);
pw.write("欢迎您!");
pw.flush();


//5、关闭资源
pw.close();
os.close();
br.close();
isr.close();
is.close();
socket.close();
serverSocket.close();

客户端:
① 创建Socket对象,指明需要连接的服务器的地址和端口号
② 连接建立后,通过输出流想服务器端发送请求信息
③ 通过输入流获取服务器响应的信息
④ 关闭响应资源

//客户端
//1、创建客户端Socket,指定服务器地址和端口
Socket socket =newSocket("localhost",10086);
//2、获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();//字节输出流
PrintWriter pw =newPrintWriter(os);//将输出流包装成打印流
pw.write("用户名:admin;密码:123");
pw.flush();
socket.shutdownOutput();
//3、获取输入流,并读取服务器端的响应信息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while((info=br.readLine())!null){
 System.out.println("我是客户端,服务器说:"+info);
}

//4、关闭资源
br.close();
is.close();
pw.close();
os.close();
socket.close();

应用多线程实现服务器与多客户端之间的通信:
① 服务器端创建ServerSocket,循环调用accept()等待客户端连接
② 客户端创建一个socket并请求和服务器端连接
③ 服务器端接受苦读段请求,创建socket与该客户建立专线连接
④ 建立连接的两个socket在一个单独的线程上对话
⑤ 服务器端继续等待新的连接

//服务器线程处理
//和本线程相关的socket
Socket socket =null;
//
public serverThread(Socket socket){
this.socket = socket;
}

publicvoid run(){
//服务器处理代码
}

//============================================
//服务器代码
ServerSocket serverSocket =newServerSocket(10086);
Socket socket =null;
int count =0;//记录客户端的数量
while(true){
socket = serverScoket.accept();
ServerThread serverThread =newServerThread(socket);
 serverThread.start();
 count++;
System.out.println("客户端连接的数量:"+count);
}

3.2 UDP模式

3.2.1 基本概念

UDP协议(用户数据报协议)是无连接的、不可靠的、无序的,速度快。 进行数据传输时,首先将要传输的数据定义成数据报(Datagram),大小限制在64k,在数据报中指明数据索要达到的Socket(主机地址和端口号),然后再将数据报发送出去

  • DatagramPacket类:表示数据报包
  • DatagramSocket类:进行端到端通信的类

3.2.2 UDP通信实现步骤

服务器端实现步骤:
① 创建DatagramSocket,指定端口号
② 创建DatagramPacket
③ 接受客户端发送的数据信息
④ 读取数据

//服务器端,实现基于UDP的用户登录
//1、创建服务器端DatagramSocket,指定端口
DatagramSocket socket =new datagramSocket(10010);
//2、创建数据报,用于接受客户端发送的数据
byte[] data =newbyte[1024];//
DatagramPacket packet =newDatagramPacket(data,data.length);
//3、接受客户端发送的数据
socket.receive(packet);//此方法在接受数据报之前会一致阻塞
//4、读取数据
String info =newString(data,o,data.length);
System.out.println("我是服务器,客户端告诉我"+info);


//=========================================================
//向客户端响应数据
//1、定义客户端的地址、端口号、数据
InetAddress address = packet.getAddress();
int port = packet.getPort();
byte[] data2 = "欢迎您!".geyBytes();
//2、创建数据报,包含响应的数据信息
DatagramPacket packet2 = new DatagramPacket(data2,data2.length,address,port);
//3、响应客户端
socket.send(packet2);
//4、关闭资源
socket.close();

客户端实现步骤:
① 定义发送信息
② 创建DatagramPacket,包含将要发送的信息
③ 创建DatagramSocket
④ 发送数据

//客户端
//1、定义服务器的地址、端口号、数据
InetAddress address =InetAddress.getByName("localhost");
int port =10010;
byte[] data ="用户名:admin;密码:123".getBytes();
//2、创建数据报,包含发送的数据信息
DatagramPacket packet = newDatagramPacket(data,data,length,address,port);
//3、创建DatagramSocket对象
DatagramSocket socket =newDatagramSocket();
//4、向服务器发送数据
socket.send(packet);


//接受服务器端响应数据
//======================================
//1、创建数据报,用于接受服务器端响应数据
byte[] data2 = new byte[1024];
DatagramPacket packet2 = new DatagramPacket(data2,data2.length);
//2、接受服务器响应的数据
socket.receive(packet2);
String raply = new String(data2,0,packet2.getLenth());
System.out.println("我是客户端,服务器说:"+reply);
//4、关闭资源
socket.close();

4.关于阻塞

先说说一些比较“轻”的避免手段:
1)发送完后调用Socket的shutdownOutput()方法关闭输出流,这样对端的输入流上的read操作就会返回-1。但是这个方法不能用于通信双方需要多次交互的情况。

2)发送数据时,约定数据的首部固定字节数为数据长度。这样读取到这个长度的数据后,就不继续调用read方法。

3)为了防止read操作造成程序永久挂起,还可以给socket设置超时。
如果read()方法在设置时间内没有读取到数据,就会抛出一个java.net.SocketTimeoutException异常。
例如下面的方法设定超时3秒。
socket.setSoTimeout(3000);

接着我们来看看比较“重”的两种解决方案:

4.1 多线程解决堵塞

主要是为了解决多个客户端同时访问引起的阻塞问题,上面已经谈到了多线程的实现,不过可以更进一步,在服务器端加上线程池,使处理更加高效:

服务器源码:

public class Server {
    public static void main(String[] args) throws IOException, InterruptedException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(9527), 10);
        
        final AtomicInteger count = new AtomicInteger(0);
        ExecutorService pool = Executors.newCachedThreadPool(new ThreadFactory() {
            public Thread newThread(Runnable r) {
                return new Thread(r, "ThreadPool-new-" + count.incrementAndGet());
            }
        });
        
        while(true) {
            try {
                final Socket clientSocket = serverSocket.accept();
                pool.execute(new Runnable() {
                    public void run() {
                        String client = clientSocket.getInetAddress().getHostAddress() + ":" + clientSocket.getPort();
                        System.out.println(client + " received!");
                        System.out.println(Thread.currentThread().getName() + " handle " + client);
                        
                        try {
                            clientSocket.setTcpNoDelay(true);
                            clientSocket.setReuseAddress(true);
                            BufferedReader bufReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                            PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());
                            
                            String line = bufReader.readLine();
                            System.out.println(client + " say:" + line);
                            
                            writer.println("received '" + line + "'");
                            writer.flush();
                            
                            Thread.sleep(10000);
                        } catch (SocketException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            try {
                                clientSocket.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

4.2 使用NIO非阻塞通信

NIO则具有非阻塞的特性,可以通过对channel的阻塞行为的配置,实现非阻塞式的信道。在非阻塞情况下,线程在等待连接,写数据等(标准IO中的阻塞操作)的同时,也可以做其他事情,这便实现了线程的异步操作。
这里有关于nio的详细说明:
https://www.jianshu.com/p/052035037297

  • 非阻塞式网络IO的特点:

1)把整个过程切换成小的任务,通过任务间协作完成。
2)由一个专门的线程来处理所有的 IO 事件,并负责分发。
3)事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
4)线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的进程切换
5)Java NIO引入了选择器的概念,选择器可以监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道,这也是非阻塞IO的核心。而在标准IO的Socket编程中,单个线程则只能在一个端口监听。
参考:

  • NIO模式的基本原理描述如下:

服务端打开一个通道(ServerSocketChannel),并向通道中注册一个选择器(Selector),这个选择器是与一些感兴趣的操作的标识(SelectionKey,即通过这个标识可以定位到具体的操作,从而进行响应的处理)相关联的,然后基于选择器(Selector)轮询通道(ServerSocketChannel)上注册的事件,并进行相应的处理。
客户端在请求与服务端通信时,也可以像服务器端一样注册(比服务端少了一个SelectionKey.OP_ACCEPT操作集合),并通过轮询来处理指定的事件,而不必阻塞。

服务器端源码示例:

public class NIOServer {
    
    private static int BUFF_SIZE=1024;
    private static int TIME_OUT = 2000;
    public static void main(String[] args) throws IOException {
        
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(10083));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        TCPProtocol protocol = new EchoSelectorProtocol(BUFF_SIZE); 
        
        while (true) {
            if(selector.select(TIME_OUT)==0){
                //在等待信道准备的同时,也可以异步地执行其他任务,  这里打印*
                System.out.print("*");  
                continue;  
            }
            Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
            while (keyIter.hasNext()) {
                SelectionKey key = keyIter.next(); 
                //如果服务端信道感兴趣的I/O操作为accept  
                if (key.isAcceptable()){  
                    protocol.handleAccept(key);  
                }  
                //如果客户端信道感兴趣的I/O操作为read  
                if (key.isReadable()){  
                    protocol.handleRead(key);  
                }  
                //如果该键值有效,并且其对应的客户端信道感兴趣的I/O操作为write  
                if (key.isValid() && key.isWritable()) {  
                    protocol.handleWrite(key);  
                }
                
                //这里需要手动从键集中移除当前的key  
                keyIter.remove(); 
            }
            
        }
    }
}



public interface TCPProtocol {

    void handleAccept(SelectionKey key) throws IOException;

    void handleRead(SelectionKey key) throws IOException;

    void handleWrite(SelectionKey key) throws IOException;

}


public class EchoSelectorProtocol implements TCPProtocol {
    
    private int bufSize; // 缓冲区的长度  
    public EchoSelectorProtocol(int bufSize){  
    this.bufSize = bufSize;  
    }

    @Override
    public void handleAccept(SelectionKey key) throws IOException {
        System.out.println("Accept");
        SocketChannel socketChannel = ((ServerSocketChannel)key.channel()).accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
        
    }

    @Override
    public void handleRead(SelectionKey key) throws IOException {
        SocketChannel clntChan = (SocketChannel) key.channel();  
        //获取该信道所关联的附件,这里为缓冲区  
        ByteBuffer buf = (ByteBuffer) key.attachment();
        buf.clear();
        long bytesRead = clntChan.read(buf);
        //如果read()方法返回-1,说明客户端关闭了连接,那么客户端已经接收到了与自己发送字节数相等的数据,可以安全地关闭  
        if (bytesRead == -1){   
            clntChan.close();  
        }else if(bytesRead > 0){
            //如果缓冲区总读入了数据,则将该信道感兴趣的操作设置为为可读可写  
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);  
        }  
        
    }

    @Override
    public void handleWrite(SelectionKey key) throws IOException {
        // TODO Auto-generated method stub
        ByteBuffer buffer=(ByteBuffer) key.attachment();
        buffer.flip();
        SocketChannel clntChan = (SocketChannel) key.channel();  
        //将数据写入到信道中  
        clntChan.write(buffer);  
        if (!buffer.hasRemaining()){
            
        //如果缓冲区中的数据已经全部写入了信道,则将该信道感兴趣的操作设置为可读  
          key.interestOps(SelectionKey.OP_READ);  
        } 
        //为读入更多的数据腾出空间  
        buffer.compact();   
        
    }

}

客户端:

public class NIOClient {
    
    public static void main(String[] args) throws IOException {
        SocketChannel clntChan = SocketChannel.open();
        clntChan.configureBlocking(false);
        if (!clntChan.connect(new InetSocketAddress("localhost", 10083))){  
            //不断地轮询连接状态,直到完成连接  
            while (!clntChan.finishConnect()){  
                //在等待连接的时间里,可以执行其他任务,以充分发挥非阻塞IO的异步特性  
                //这里为了演示该方法的使用,只是一直打印"."  
                System.out.print(".");    
            }  
         }
        
        //为了与后面打印的"."区别开来,这里输出换行符  
        System.out.print("\n");  
        //分别实例化用来读写的缓冲区  
        
        ByteBuffer writeBuf = ByteBuffer.wrap("send send send".getBytes());
        ByteBuffer readBuf = ByteBuffer.allocate("send".getBytes().length-1);
        
        while (writeBuf.hasRemaining()) {
            //如果用来向通道中写数据的缓冲区中还有剩余的字节,则继续将数据写入信道  
                clntChan.write(writeBuf);  
            
        }
        StringBuffer stringBuffer=new StringBuffer(); 
        //如果read()接收到-1,表明服务端关闭,抛出异常  
            while ((clntChan.read(readBuf)) >0){
                readBuf.flip();
                stringBuffer.append(new String(readBuf.array(),0,readBuf.limit()));
                readBuf.clear();
            }  
        
      //打印出接收到的数据  
        System.out.println("Client Received: " +  stringBuffer.toString());  
        //关闭信道  
        clntChan.close();  
    }
}

参考引用:

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

推荐阅读更多精彩内容