Android开发Android技术知识Android开发

Android Socket 通信

2018-07-14  本文已影响33人  秀花123

一、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 协议有以下特点:

2.Datagram Sockets(数据报套接字)
基于 UDP协议,采用 数据报文 提供数据打包发送的服务。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()

其它方法:

四、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) 

常用方法:

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

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

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

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

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

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

短连接

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

长连接

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

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:

    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%时就持续告警,留出充足的反应时间。

int nRecvBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
上一篇下一篇

猜你喜欢

热点阅读