Android Socket 通信

一、Socket

Socket 作为一种通用的技术规范,首次是由 Berkeley 大学在 1983 为 4.2BSD Unix 提供的,后来逐渐演化为 POSIX 标准。Socket API 是由操作系统提供的一个编程接口,让应用程序可以控制使用 socket 技术。
Socket API 不属于 TCP/IP协议簇,只是操作系统提供的一个是一个对 TCP / IP协议进行封装 的编程调用接口,工作在应用层与传输层之间:
一个 Socket 包含两个必要组成部分:

  1. 地址:IP 和端口号组成一队套接字
  2. 协议:Socket 所用的是传输层协议,目前有 TCP、UDP、raw IP

协议

根据传输方式不同(即使用的协议不同)可分为三种:
1.Stream Sockets(流套接字)
基于 TCP协议,采用 流的方式 提供可靠的字节流服务。TCP 协议有以下特点:

  • 面向连接:指的是要使用TCP传输数据,必须先建立TCP连接,传输完成后释放连接,就像打电话一样必须先拨号建立一条连接,打完后挂机释放连接。
  • 全双工通信:即一旦建立了TCP连接,通信双方可以在任何时候都能发送数据。
  • 可靠的:指的是通过TCP连接传送的数据,无差错,不丢失,不重复,并且按序到达。
  • 面向字节流:流,指的是流入到进程或从进程流出的字符序列。简单来说,虽然有时候要传输的数据流太大,TCP报文长度有限制,不能一次传输完,要把它分为好几个数据块,但是由于可靠性保证,接收方可以按顺序接收数据块然后重新组成分块之前的数据流,所以TCP看起来就像直接互相传输字节流一样,面向字节流。

2.Datagram Sockets(数据报套接字)
基于 UDP协议,采用 数据报文 提供数据打包发送的服务。UDP 协议有以下特点:

  • 无连接的:和TCP要建立连接不同,UDP传输数据不需要建立连接,就像写信,在信封写上收信人名称、地址就可以交给邮局发送了,至于能不能送到,就要看邮局的送信能力和送信过程的困难程度了。
  • 不可靠的:因为UDP发出去的数据包发出去就不管了,不管它会不会到达,所以很可能会出现丢包现象,使传输的数据出错。
  • 面向报文:数据报文,就相当于一个数据包,应用层交给UDP多大的数据包,UDP就照样发送,不会像TCP那样拆分。
  • 没有拥塞控制:拥塞,是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象,就像交通堵塞一样。TCP建立连接后如果发送的数据因为信道质量的原因不能到达目的地,它会不断重发,有可能导致越来越塞,所以需要一个复杂的原理来控制拥塞。而UDP就没有这个烦恼,发出去就不管了。

3.Row Sockets
通常用在路由器或其他网络设备中,这种 socket 不经过TCP/IP协议簇中的传输层(transport layer),直接由网络层(Internet layer)通向应用层(Application layer),所以这时的数据包就不会包含 tcp 或 udp 头信息。
Android网络编程:基础理论汇总

二、Socket 基本用法

1、TCP 服务器端

protected void TCPServer(){
        try {
            //创建服务器端 Socket,指定监听端口
            ServerSocket serverSocket = new ServerSocket(8888);
            //等待客户端连接
            Socket clientSocket = serverSocket.accept();
            //获取客户端输入流,
            InputStream is = clientSocket.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            String data = null;
            //读取客户端数据
            while((data = br.readLine()) != null){
                System.out.println("服务器接收到客户端的数据:" + data);
            }
            //关闭输入流
            clientSocket.shutdownInput();
            //获取客户端输出流
            OutputStream os = clientSocket.getOutputStream();
            PrintWriter pw = new PrintWriter(os);
            //向客户端发送数据
            pw.print("服务器给客户端回应的数据");
            pw.flush();
            //关闭输出流
            clientSocket.shutdownOutput();
            //关闭资源
            pw.checkError();
            os.close();
            br.close();
            isr.close();
            is.close();
            clientSocket.close();
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

2、TCP 客户端

protected void TCPClient(){
        try {
            //创建客户端Socket,指定服务器的IP地址和端口
            Socket socket = new Socket(InetAddress.getLocalHost(),8888);
            //获取输出流,向服务器发送数据
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(os);
            pw.write("客户端给服务器端发送的数据");
            pw.flush();
            //关闭输出流
            socket.shutdownOutput();

            //获取输入流,接收服务器发来的数据
            InputStream is = socket.getInputStream();
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            String data = null;
            //读取客户端数据
            while((data = br.readLine()) != null){
                System.out.println("客户端接收到服务器回应的数据:" + data);
            }
            //关闭输入流
            socket.shutdownInput();

            //关闭资源
            br.close();
            isr.close();
            is.close();
            pw.close();
            os.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

3、UDP 服务端

protected void UDPServer(){
        try {
            //创建服务器端 Socket,指定端口
            DatagramSocket socket = new DatagramSocket(8888);
            //创建数据报用于接收客户端发送的数据
            byte[] bytes = new byte[1024];
            DatagramPacket packet = new DatagramPacket(bytes,bytes.length);
            //接收客户端发送的数据
            socket.receive(packet);
            //读取数据(也可以调用 packet.getData())
            String info = new String(bytes,0,packet.getLength());

            //返回数据
            InetAddress address = packet.getAddress();
            int port = packet.getPort();
            byte[] data = "服务器返回的数据".getBytes();
            DatagramPacket dataPacket = new DatagramPacket(data,data.length,address,port);
            socket.send(dataPacket);
            //关闭 Socket
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

4、UDP 客户端

 protected void UDPClient(){
        try {
            //创建客户端 Socket
            DatagramSocket socket = new DatagramSocket();
            //创建数据包
            byte[] data = "向服务器发送的数据".getBytes();
            InetAddress address = InetAddress.getLocalHost();
            int port = 8888;
            DatagramPacket packet = new DatagramPacket(data,data.length,address,port);
            //发送数据包
            socket.send(packet);
            
            //接收服务器响应的数据包
            byte[] info = new byte[1024];
            DatagramPacket infoPacket = new DatagramPacket(info,info.length);
            String receiveInfo = new String(info,0,infoPacket.getLength());
            
            socket.close();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

三、InetAddress 类

InetAddress 是 Java 对 IP 地址的封装,InetAddress 的实例对象包含以数字形式保存的 IP 地址,同时还可能包含主机名(如果使用主机名来获取 InetAddress 的实例,或者使用数字来构造,并且启用了反向主机名解析的功能)。InetAddress 类提供了将主机名解析为IP地址(或反之)的方法。

InetAddress 对象的获取

InetAddress的构造函数不是公开的(public),所以需要通过它提供的静态方法来获取,有以下的方法:

//返回代表由一个特殊名称分解的所有地址的InetAddresses类数组
//在不能把名称分解成至少一个地址时,它将引发一个UnknownHostException异常。
static InetAddress[] getAllByName(String host)

static InetAddress getByAddress(byte[] addr)

static InetAddress getByAddress(String host,byte[] addr)
//返回一个传给它的主机名的InetAddress。
//如果这些方法不能解析主机名,它们引发一个UnknownHostException异常。
static InetAddress getByName(String host)
//仅返回象征本地主机的InetAddress对象,
//本机地址还为localhost,127.0.0.1,这三个地址都是一回事。
static InetAddress getLocalHost()

其它方法:

  • public String getHostName()
    根据创建 InetAddress 对象的不同方式,getHostName 的返回值是不同的。
    用 getLocalHost() 方法创建的 InetAddress 的对象,返回的是本机名;
    用域名作为 getByName 和 getAllByName 方法的参数得到的 InetAddress 对象,该对象会得到这个域名,当调用 getHostName 时,就无需再访问 DNS 服务器,而是直接将这个域名返回。
    使用IP地址创建 InetAddress 对象(getByName,getAllByName,getByAddress)方法都可以通过 IP 地址创建 InetAddress 对象)时,并不需要访问 DNS 服务器。因此,通过 DNS 服务器查找域名的工作就由 getHostName 方法来完成。 如果 IP 地址不存在或 DNS 服务器不允许进行 IP 地址和域名映射,就返回这个 IP 地址。
  • public String getHostAddress()
    该方法用来得到主机的 IP 地址,这个 IP 地址可以是 IPv4 也可以是 IPv6 的
  • public byte[] getAddress()
    该方法和 getHostAddress 方法唯一区别是,getHostAddress 返回字符形式的 IP 地址,getAddress 返回 byte 数组形式的 IP 地址。

四、URL 类

类 URL 代表一个统一资源定位符,包括协议、主机名
构造方法:

//url 代表一个绝对地址,URL 对象直接指向这个资源
URL ( String url)
//baseURL 代表绝对地址,relativeURL 代表相对地址
URL ( URL baseURL , String relativeURL) 
//protocol 代表通信协议,host 代表主机名,file 代表文件名
 URL ( String protocol , String host , String file) 
//
 URL ( String protocol , String host , int port , String file) 

常用方法:

  • url1.getHost() 主机
  • getProtocol()协议
  • getPort()端口,如果未指定端口号,则使用默认的端口号,此时 getPort() 方法返回值为-1
  • getPath()文件路径
  • getFile()文件名
  • getRef() 相对路径
  • getContent()内容
  • getQuery() 查询字符串
  • openStream() 通过 URL 的 openStream 方法获取 URL 对象所表示的资源的字节输入流

五、Socket 连接和 Http 连接的关系

Socket 连接一般情况下都是 TCP 连接,因此 Socket 连接一旦建立,通信双方就可以进行互相发送内容。但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。(这也就是常说的“心跳策略”)
Http连接是 “请求-响应” 的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

总结:如果建立的是Socket连接,服务器可以直接将数据传送给客户端;如果方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端。

六、长连接、短连接、轮询和心跳

在HTTP/1.0中,默认使用的是短连接。但从 HTTP/1.1起,默认使用长连接。

HTTP 是一种应用层的网络协议,长连接是存在于网络层的一种连接状态,而实现它则需要在传输层进行开发。
HTTP 作为应用层协议,其实它的生命周期在服务器返回结果时就已经结束了,而所谓的支持长连接,其实是基于 'Keep-Alive' 请求头所约定,从而向下进行长连接发起的一种机制。该长连接依然是基于 TCP 的。

短连接

所谓短连接,即连接只保持在数据传输过程,请求发起,连接建立,数据返回,连接关闭。它适用于一些实时数据请求,配合轮询来进行新旧数据的更替。

长连接

长连接便是在连接发起后,在请求关闭连接前客户端与服务端都保持连接,不管此时有无数据包的发送,实质是保持这个通信管道,之后便可以对其进行复用。
它适用于涉及消息推送,请求频繁的场景(直播,流媒体)。连接建立后,在该连接下的所有请求都可以重用这个长连接管道,避免了频繁了连接请求,提升了效率。
长连接的优势:

  • 减少连接建立过程的耗时大家都知道TCP连接建立需要三次握手,三次握手也就说需要三次交互才能建立一个连接通道,同城的机器之间的大概是ms级别的延时,影响还不大,如果是北京和上海两地机房,走专线一来一回大概需要30ms,如果使用长连接,这个优化还是十分可观的。
  • 方便实现push数据数据交互-推模式实现的前提是网络长连接,有了长连接,连接两端很方便的互相push数据,来进行交互。

TCP连接在默认的情况下就是所谓的长连接, 也就是说连接双方都不主动关闭连接, 这个连接就应该一直存在.

长连接怎么保活?
TCP 协议实现中,是有保活机制的,也就是 TCP 的 KeepAlive 机制(此机制并不是 TCP 协议规范中的内容,由操作系统去实现),KeepAlive 机制开启后,在一定时间内(一般时间为7200s,参数 tcp_keepalive_time)在链路上没有数据传送的情况下,TCP层将发送相应的 KeepAlive 探针以确定连接可用性,探测失败后重试10(参数 tcp_keepalive_probes)次,每次间隔时间75s(参数 tcp_keepalive_intvl),所有探测失败后,才认为当前连接已经不可用。这些参数是机器级别,可以调整。
一个可靠的系统,长连接的保活肯定是要依赖应用层的心跳来保证的。这里应用层的心跳举个例子,比如客户端每隔3s通过长连接通道发送一个心跳请求到服务端,连续失败5次就断开连接。这样算下来最长15s就能发现连接已经不可用,一旦连接不可用,可以重连,也可以做其他的failover处理,比如请求其他服务器。

心跳

用来检测一个系统是否存活或者网络链路是否通畅的一种方式, TCP 长连接本质上不需要心跳包来维持,其一般做法是定时向被检测系统发送心跳包,被检测系统收到心跳包进行回复,收到回复说明对方存活。
心跳能够给长连接提供保活功能,能够检测长连接是否正常(这里所说的保活不能简单的理解为保证活着,具体来说应该是一旦链路死了,不可用了,能够尽快知道,然后做些其他的高可用措施,来保证系统的正常运行)。
被连接方检测心跳的实现分为心跳的发送和心跳的检测,心跳由谁来发都可以,也可以双方都发送,但是检测心跳,必须由发起连接的这端进行,才安全。因为只有发起连接的一端检测心跳,知道链路有问题,这时才会去断开连接,进行重连,或者重连到另一台服务器。

轮询

所谓轮询,即是在一个循环周期内不断发起请求来得到数据的机制。只要有请求的的地方,都可以实现轮询,譬如各种事件驱动模型。它的长短是在于某次请求的返回周期。

  • 短轮询:短轮询指的是在循环周期内,不断发起请求,每一次请求都立即返回结果,根据新旧数据对比决定是否使用这个结果。
  • 长轮询:而长轮询即是在请求的过程中,若是服务器端数据并没有更新,那么则将这个连接挂起,直到服务器推送新的数据,再返回,然后再进入循环周期。

轮询是为了获取数据, 而心跳是为了保活TCP连接.

由上可以看到,长短轮询的理想实现都应当基于长连接

UDP 广播

地址:广播地址是由 IP 地址和子网掩码(两者都是4字节)计算出来的。子网掩码的二进制形式是高 N 位1和低 (32-N) 位0。IP 地址与子网掩码进行按位与操作后得到网络号,网络号相同的 IP 地址认为在同一网段。子网掩码的所有位取反后,与网络号进行同或操作,就是广播地址了。

UDP 数据包长度:

udp 的最大包长度是 2^16-1 的个字节。由于 udp 包头占8个字节,而在 ip 层进行封装后的 ip 包头占去20字节,所以这个是 udp 数据包的最大理论长度是2^16-1-8-20=65507。然而这个只是 udp 数据包的最大理论长度,UDP 属于运输层,在传输过程中,udp 包的整体是作为下层协议的数据字段进行传输的,它的长度大小受到下层 ip 层和数据链路层协议的制约。

MTU

以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的。这个1500字节被称为链路层的 MTU (最大传输单元)。
因特网协议允许 IP 分片,这样就可以将数据包分成足够小的片段以通过那些最大传输单元小于该数据包原始大小的链路了。这一分片过程发生在网络层,它使用的是将分组发送到链路上的网络接口的最大传输单元的值。
对于大于这个数值的分组可能被分片,否则无法发送,而分组交换的网络是不可靠的,存在着丢包。不超过MTU的分组是不存在分片问题的。
MTU的值并不包括链路层的首部和尾部的18个字节。所以,这个1500字节就是网络层IP数据报的长度限制。因为IP数据报的首部为20字节,所以IP数据报的数据区长度最大为1480字节。而这个1480字节就是用来放TCP传来的TCP报文段或UDP传来的UDP数据报的。又因为UDP数据报的首部8字节,所以UDP数据报的数据区最大长度为1472字节。这个1472字节就是我们可以使用的字节数。
因为 Internet 上的路由器可能会将 MTU 设为不同的值。如果我们假定 MTU 为1500来发送数据的,而途经的某个网络的 MTU 值小于1500字节,那么系统将会使用一系列的机制来调整 MTU 值,使数据报能够顺利到达目的地。鉴于Internet上的标准 MTU 值为576字节,所以在进行 Internet 的 UDP 编程时,最好将 UDP 的数据长度控件在548字节(576-8-20)以内。

UDP丢包

udp 丢包是指网卡接收到数据包后,linux 内核的 tcp/ip 协议栈在 udp 数据包处理过程中的丢包,主要原因有两个:

  • udp数据包格式错误或校验和检查失败。
  • 应用程序来不及处理udp数据包。
    通用的udp丢包检测方法,使用netstat命令,加-su参数。
Udp:

    2495354 packets received

    2100876 packets to unknown port received.

    3596307 packet receive errors

    14412863 packets sent

    RcvbufErrors: 3596307

    SndbufErrors: 0

从上面的输出中,可以看到有一行输出包含了"packet receive errors",如果每隔一段时间执行 netstat -su,发现行首的数字不断变大,表明发生了udp丢包。
应用程序来不及处理而导致udp丢包的常见原因:
1)linux内核socket缓冲区设的太小
cat /proc/sys/net/core/rmem_default
cat /proc/sys/net/core/rmem_max
可以查看socket缓冲区的缺省值和最大值。
rmem_default和rmem_max设置为多大合适呢?如果服务器的性能压力不大,对处理时延也没有很严格的要求,设置为1M左右即可。如果服务器的性能压力较大,或者对处理时延有很严格的要求,则必须谨慎设置rmem_default 和rmem_max,如果设得过小,会导致丢包,如果设得过大,会出现滚雪球。
2)服务器负载过高,占用了大量cpu资源,无法及时处理linux内核socket缓冲区中的udp数据包,导致丢包。
一般来说,服务器负载过高有两个原因:收到的udp包过多;服务器进程存在性能瓶颈。如果收到的udp包过多,就要考虑扩容了。服务器进程存在性能瓶颈属于性能优化的范畴,这里不作过多讨论。
3)磁盘IO忙
服务器有大量IO操作,会导致进程阻塞,cpu都在等待磁盘IO,不能及时处理内核socket缓冲区中的udp数据包。如果业务本身就是IO密集型的,要考虑在架构上进行优化,合理使用缓存降低磁盘IO。
4)物理内存不够用,出现swap交换
swap交换本质上也是一种磁盘IO忙,因为比较特殊,容易被忽视,所以单列出来。
只要规划好物理内存的使用,并且合理设置系统参数,可以避免这个问题。
5)磁盘满导致无法IO
没有规划好磁盘的使用,监控不到位,导致磁盘被写满后服务器进程无法IO,处于阻塞状态。最根本的办法是规划好磁盘的使用,防止业务数据或日志文件把磁盘塞满,同时加强监控,例如开发一个通用的工具,当磁盘使用率达到80%时就持续告警,留出充足的反应时间。

  • 接收端处理时间过长导致丢包:调用recv方法接收端收到数据后,处理数据花了一些时间,处理完后再次调用recv方法,在这二次调用间隔里,发过来的包可能丢失。对于这种情况可以修改接收端,将包接收后存入一个缓冲区,然后迅速返回继续recv。
  • 发送的包巨大丢包:虽然send方法会帮你做大包切割成小包发送的事情,但包太大也不行。例如超过50K的一个udp包,不切割直接通过send方法发送也会导致这个包丢失。这种情况需要切割成小包再逐个send。
  • 发送的包较大,超过接受者缓存导致丢包:包超过mtu size数倍,几个大的udp包可能会超过接收者的缓冲,导致丢包。这种情况可以设置socket接收缓冲。以前遇到过这种问题,我把接收缓冲设置成64K就解决了。
int nRecvBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
  • 发送的包频率太快:虽然每个包的大小都小于mtu size 但是频率太快,例如40多个mut size的包连续发送中间不sleep,也有可能导致丢包。这种情况也有时可以通过设置socket接收缓冲解决,但有时解决不了。所以在发送频率过快的时候还是考虑sleep一下吧。
  • 局域网内不丢包,公网上丢包。这个问题我也是通过切割小包并sleep发送解决的。如果流量太大,这个办法也不灵了。总之udp丢包总是会有的,如果出现了用我的方法解决不了,还有这个几个方法: 要么减小流量,要么换tcp协议传输,要么做丢包重传的工作。
    UDP主要丢包原因及具体问题分析