最近在研究Socket的通信,感觉坑很多,多,多。。所以今天就和大家来简单分享下Socket的使用方式,以及关于Socket的几个比较重要,容易被小伙伴们忽略且常用的方法,
好了,进入今天的正题。
在Android中,像 http请求、socket通信等等都属于网络通信的一种方式。http请求底层也是socket的一种实现方式。
我们今天的主题会围绕在Android中使用Socket来实现通信。
说到通信,可以简单理解为两方相互交流。双方即手机和远程服务器,我们可以把手机当做客户端,因为我们要与”遥远的东方“发消息。此时手机可以作为一个Socket端。
那么“遥远的东方”就是服务器端,即ServerSocket(服务端Socket)。这样当客户端Socket和服务端ServerSocket建立了联系之后,双方都确定了对方的身份,证明大家都是认识的是朋友。才可以进行“悄悄话的交流”。这里我们提到要双方确认,其实在Socket中作为三次握手,下面我们来分析Socket与服务端ServerSocket建立联系的三次握手过程:
(1)第一次握手:建立连接时,客户端Socket向服务端ServerSocket发送SYN
包,并进入SYN_SEND状态,等待服务器B确认。这个过程,就好比我向你打电话,我的手机号通过信号到了你的手机上并显示
156XXXXXXXX来电,此时要等你确认这个手机号。
(2)第二次握手:此时服务端收到客户端的SYN包,与客户端的SYN进行确认,确认后,服务端向客户端发送个SYN包,即SYN+ACK
包。服务端进入SYN_RECV状态。这个过程就好比,当收到某人的来电后,我确认手机号是我的朋友,此时我要接通他的电话。
(3)第三次握手:客户端收到服务端发送过来的SYN + ACK包,此时客户端像服务端再发送一个ACK确认包。此时发送完毕后,客户端与服务端就进入了ESTABLISHED状态,完成通信前的三次握手。这个过程就好比,当我接通了电话后,我问对方是XXX吗?对方说是的!OK啦,那接下来就是要通信的内容了。
至此,在Socket与ServerSocket进行通信前的准备工作就完成了。现在我们就可以进行通信交流了,接下来看看我们该如何使用。
因为Socket是属于Java的net。所以在Android中使用的方式是一样的。首先,我们需要建立双方,即创建客户端Socket和服务器端ServerSocket:
(1)创建客户端Socket:
在上述代码中,我们首先来看InetSocketAddress这个类。InetSocketAddress继承了SocketAddress类。该类从上述代码中我们就能看出来它的作用,没错,就是用于指定我们需要连接服务器的地址。我们可以使用它的构造函数来指定要连接的服务器IP和Port
(ip地址和端口号)。继续往下看,此时我们创建一个Socket实例,并调用了Socket的一些属性方法。最后通过Socket的connect()方法来根据InetSocketAddress进行连接服务器。过程很简单,核心就是需要我们去指定InetSocketAddress中的ip和port。简单分析完流程,我们来具体分析Socket的这几个核心方法:
在JDK1.4中共有8个Socket方法可以设置。这8个方法对应了8个属性选项,它们都定义在Java.net.SocketOptions接口中。定义如下:
public final static int TCP_NODELAY = 0x0001;
public final static int SO_REUSEADDR = 0x04; public final static int SO_LINGER = 0x0080; public final static int SO_TIMEOUT = 0x1006; public final static int SO_SNDBUF = 0x1001; public final static int SO_RCVBUF = 0x1002; public final static int SO_KEEPALIVE = 0x0008; public final static int SO_OOBINLINE = 0x1003; |
从上面看到,除TCP_NODELAY以为,其他都是以SO开头。 上面的这8个成员变量分别提供set和get方法来操作,即对应了我们上面调用的这几个方法:
(1)setSoTimeOut:设置客户端Socket读取数据的超时时长。即当与服务端建立联系后,此时要接收服务端返回的数据,设置该方法可以限定客户端等待服务端发送数据的时间,如果超过了该时长,系统会抛出一个InterruptedIOException异常。在抛出异常后,输入流并未关闭,你可以继续通过read方法读取数据。 如果将timeout设为0,就意味着read将会无限等待下去,直到服务端程序关闭这个Socket.这也是timeout的默认值。
(2)setKeepAlive:该方法指定检测与服务器的链接状态。如果设置参数为true,即打开该设置,此时,客户端会利用空闲的连接每隔两个小时向服务端发送一次数据包进行连接验证服务器是否仍处于活动状态。如果此时服务端未响应,客户端会在第11分钟后继续发送一个验证包,如果在12分钟内服务端还未响应,此时客户端就会执行关闭连接的操作。如果将该设置项关闭,客户端Socket在服务器无效的情况下可能会长时间不会关闭,占据资源。默认情况下是关闭的。
(3)setTcpNoDelay:该方法很有聪明,如果开启该设置,当客户端发送数据给服务端时,发送的过程中,会检测该数据的大小,如果该数据较小,此时不会发送给服务端,而是将较小的包和较大的数据包合,然后一起发送给服务端。在发送下一个数据包时,系统会等待服务器对前一个数据包的响应,当收到服务器的响应后,再发送下一个数据包,这就是所谓的Nagle算法;优点很明显,节省了通信的开销,有效地改善网络传输的效率。在默认情况下是开启的。
(4)setSoLinger:其实该方法和Socket的关闭方法(
close() )是有联系的。该方法有两个参数,第一个如果设置为true,即开启该设置。第二个参数指定一个时间值(秒:0 ~ 65535,不可为负值)。该方法的作用是当你调用了Socket的close方法时,系统会去检测是否还有未发送完毕的数据,此时如果存在未发送完毕的数据,系统就会在我们指定的时间内 “努力”发送这些数据到服务器,如果在我们指定的时间内还未发送完毕,那么此时Socket将会执行关闭。如果底层的Socket实现不支持SO_LINGER都会抛出SocketException,可以通过getSoLinger方法来获取延迟关闭的时间,如果返回
-1,则表明SO_LINGER是关闭的。
(5)setReceiveBufferSize:该方法比较简单,设置输入流的缓冲大小。默认情况下,输入流的接收缓冲区是8096个字节(8K)。这个值是Java所建议的输入缓冲区的大小。缓冲值尽量不要设置的太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。如果底层不支持,系统将会抛出IllegalArgumentException异常。
(6)setSendBufferSize:此方法和上面的对称,设置输出流的缓冲大小。其他特点和上面相同,不再赘述。
(7)setReuseAddress:如果你的服务程序停止后想立即重启,不等60秒,而新套接字依旧 使用同一端口,此时
SO_REUSEADDR 选项非常有用。
(8)setOOBInline:Socket类的sendUrgentData方法向服务器发送一个单字节的数据。这个单字节数据并不经过输出缓冲区,而是立即发出。
一般情况下,几个设置项我们都选择开启状态,某些情况下,还需要具体情况具体分析。
上面详细介绍了Socket的几个重要的核心方法,接下来看服务端的创建:
客户端的创建非常简单,只需要我们指定具体的端口号就行。
分析完了具体的创建过程,我们来看下Socket和ServerSocket是怎么进行通信的。
Socket采用流的形式来进行通信,OutputStream、InputStream、ObjectInputStream、ObjectOutputStream。
(1)OutputStream:输出流。
(2)InputStream:输入流。
(3)ObjectInputStream:对象输入流。使用该流写入实例对象时,实例对象需要实现Serializable。
(4)ObjectOutputStream:对象输出流。使用该流写出实例对象时,实例对象需要实现Serializable。
下面看客户端发送数据给服务端:
很简单,就是流读写的基本操作,不再赘述。
看服务端接收数据并返回数据的过程:
在上面代码中我们看到,首先我们开启了接收数据的一个线程。在线程中我们无限循环的去执行accept()方法。该方法是服务端接收客户端数据的方法,返回值是客户端的Socket实例。该方法在没有接收到客户端发送过来的数据时将会一直处于阻塞状态,直到接收到客户端发送的数据才会继续向下执行,接收到客户端的Socket,继续执行流的读写操作来读取和写回数据。
一个完美的Socket通信我们就介绍完了,在实际的开发过程中,因为服务端是面向多个客户端的,即 1 - N 的关系。所以我们还需要接收到客户端发来的数据后,开启对应的线程去处理对应的逻辑任务。客户端也是一样,当接收到服务器的数据时,同样需要开启新的线程来处理服务端发来的逻辑任务。
好了,关于Socket的通信我们就简单分析到这里,如果有疑问的童靴可以和我联系一起讨论,欢迎大家拍砖!