【我的Android进阶之旅】关于TCP、UDP协议,Socket编程
最近事情比较多,也因为懒惰导致,好久没写博客了,不能再放任自己了。。。。。。。O(∩_∩)O哈哈~。
网络编程基础
TCP/IP协议
我们先看看从宏观上来看两台机器是如何通信的。
我们通过QQ和服务器进行通信,都需要哪些东西呢?
两台主机进行通信,需要知道双方电脑的的地址(也就是IP地址);知道两个电脑的地址之后,我们还需要知道我发送到目的电脑的目的软件(使用端口标记)。这样两台电脑连接成功之后就可以进行通信了。
那么这些东西例如:目的地如何规定,发送的数据如何包装,放到哪里?这中间就需要有各种协议。大家都使用这个协议,统一成一个规范,这样符合这个规范的各种设备之间能够进行兼容性的通信。
最为广泛的的协议就是OSI协议和TCP/IP协议了,但是OSI协议较为繁琐,未推广(想了解的自己Google)。反而TCP/IP(transfer control protocol/internet protocol,传输控制协议/网际协议)协议简单明了,得到现今的广泛使用。
TCP/IP准确的说是一组协议,是很多协议的集合,是一组协议集合的简称。来看看:
名称 | 协议 | 功能 | |
---|---|---|---|
应用层 | HTTP、Telnet、FTP、TFTP | 提供应用程序网络接口 | |
传输层 | TCP、UDP | 建立端到端的连接 | |
网络层 | IP | 寻址和路由 | |
数据链路层 | Ethernet、802.3、PPP | 物理介质访问 | |
物理层 | 接口和电缆 | 二进制数据流传输 |
下面以QQ的数据传输为例子:
QQ的数据传输
IP地址、端口
我们知道端到端的连接提到了几个关键的字眼:IP地址、端口;
IP地址用来标记唯一的计算机位置,端口号用来标记一台电脑中的不同应用程序。
其中IP地址是32为二进制,例如:192.168.0.1等等,这个组合方式是一种协议拼起来的,详情Google。
端口号范围是065536,其中01023是系统专用,例如:
协议名称 | 协议功能 | 默认端口号 |
---|---|---|
HTTP(HypertextTransfer Protocol)超文本传输协议 | 浏览网页 | 80 |
FTP(File TransferProtocol) 文件传输协议 | 用于网络上传输文件 | 21 |
TELNET | 远程终端访问 | 23 |
POP3(Post OfficeProtocol) | 邮局协议版本 | 110 |
IP地址和端口号组成了我们的Socket,也就是“套接字”,Socket只是一个API。
Socket原理机制:
通信的两端都有Socket
网络通信其实就是Socket间的通信
数据在两个Socket间通过IO传输
单独的Socke是没用任何作用的,基于一定的协议(比如:TCP、UDP协议)下的socket编程才能使得数据畅通传输,下面我们就开始吧。
基于TCP(传输控制协议)协议的Socket编程
以下将“基于TCP(传输控制协议)协议的Socket编程”简称为TCP编程
既然基于TCP,那么就有着它的一套代码逻辑体系。我们只需要在Socket API的帮助下,使用TCP协议,就可以进行一个完整的TCP编程了。
主要API:
Socket,客户端相关
- 构造方法
public Socket(String host, int port) throws UnknownHostException, IOException
释义:创建一个流套接字并将其连接到指定主机上的指定端口号(就是用来连接到host主机的port端口的) - 方法
|方法名称 | 方法功能|
| ------------- :|-------------:|
|getInputStream()) | 拿到此套接字的输入流,收到的数据就在这里 |
|getOutputStream()| 返回此套接字的输出流。 要发送的数据放到这里|
ServerSocket,服务器相关
- 构造方法
ServerSocket(int port)
释义:创建服务端的监听port端口的套接字 - 方法
Socket accept() throws IOException
侦听并接受到此套接字的连接。此方法在连接传入之前一直阻塞。服务端通过这个方法拿到与客户端建立端到端的连接的socket。
总体流程图示:
Socket通信流程
- TCP编程的服务端流程:
1.创建ServerSocket类对象-serverSocket
2.使用serverSocket开始一直阻塞监听,等待客户端发送来数据并且得到socket
3.根据socket的输入流读取客户端的数据,根据socket的输出流返回给客户端数据
4.关闭socket以及IO流 - TCP编程的客户端对象
1.创建客户端的socket对象
2.使用客户端的socket对象的输出流发送给服务器数据,使用客户端的socket对象的输入流得到服务端的数据
TCP编程
下面我们使用上面的TCP编程的流程来实现:手机发送信息到服务器,服务器返回给我们数据。
服务器端主要代碼
InputStream is = null;
InputStreamReader reader = null;
BufferedReader bufferedReader = null;
OutputStream os = null;
try {
is = socket.getInputStream();
reader = new InputStreamReader(is);
bufferedReader = new BufferedReader(reader);
String s = null;
StringBuffer buffer = new StringBuffer();
while ((s=bufferedReader.readLine())!=null){
buffer.append(s);
}
System.out.println("服务器收到信息:"+buffer.toString());
// 关闭输入流
socket.shutdownInput();
os = socket.getOutputStream();
os.write("我是服务器发来的信息".getBytes());
os.flush();
socket.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(os!=null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bufferedReader!=null){
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(reader!=null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:
在使用TCP编程的时候,最后需要释放资源,关闭socket(socket.close()
);关闭socket输入输出流(socket.shutdownInput()以及socket.shutdownOutput()
);关闭IO流(is.close() os.close()
)。需要注意的是:关闭socket的输入输出流需要放在关闭io流之前。因为, <u>**关闭IO流会同时关闭socket,一旦关闭了socket的,就不能再进行socket的相关操作了。而,只关闭socket输入输出流(socket.shutdownInput()以及socket.shutdownOutput()
)不会完全关闭socket,此时任然可以进行socket方面的操作。 **</u>所以要先调用socket.shutdownXXX,然后再调用io.close();
客户端:
页面文件没什么好看的。然后就是点击button的时候发送数据,收到数据展示出来。我们这里主要看点击按钮时做的事情。
客户端主要代碼
new Thread(new Runnable() {
@Override
public void run() {
Log.e("wzytest","tcpConnect........");
try {
socket = new Socket("192.168.137.1",12888);
os = socket.getOutputStream();
os.write("这是客户端消息".getBytes());
os.flush();
socket.shutdownOutput();
is = socket.getInputStream();
reader = new InputStreamReader(is);
bufferedReader = new BufferedReader(reader);
String s = null;
stringBuffer = new StringBuffer();
while ((s=bufferedReader.readLine())!=null){
stringBuffer.append(s);
}
Log.e("wzytest","客户端收到来自服务器的消息:"+stringBuffer.toString());
socket.shutdownInput();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
is.close();
os.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
注意!
开发中的关闭IO资源需要放到finally中。
整体运行结果如下:
TCP的单线程编程
在上图中,我们手机端发送完一个请求后,服务端(Server)拿到数据,解析数据,返回给客户端数据,关闭所有资源,也就是服务器关闭了。这时,如果另一个客户端再想跟服务器进行通信时,发现服务器已经关闭了,无法与服务器再次进行通信。换句话说,只能跟服务器通信一次,服务端 只能支持单线程数据处理。也就是说,上面的服务器的代码无法实现多线程编程,只能进行一次通信。
那么如果我们想实现server的多线程数据处理,使得server处理完我这个请求后不会关闭,任然可以处理其他客户端的请求,怎么办呢?
TCP的多线程编程
思路:
在上面例子中,我们执行serversocket.accept()
等待客户端去连接,与客户建立完连接后,拿到对应的socket,然后进行相应的处理。那么多个客户端的请求,我们就一直不关闭ServerSocket,一直等待客户端连接,一旦建立连接拿到socket,就可以吧这个socket放到单独的线程中,从而实现这个建立连接的端到端通信的socket在自己单独的线程中处理。这样就能实现Socket的多线程处理。
- step1:
创建ServerThread,单独处理拿到的socket,使得客户端到服务器端的这个socket会话在一个单独的线程中。
package com.genlot.work.tcpippro;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
class ServerThread extends Thread{
private Socket socket ;
public ServerThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
super.run();
InputStream is = null;
InputStreamReader reader = null;
BufferedReader bufferedReader = null;
OutputStream os = null;
try {
is = socket.getInputStream();
reader = new InputStreamReader(is);
bufferedReader = new BufferedReader(reader);
String s = null;
StringBuffer buffer = new StringBuffer();
while ((s=bufferedReader.readLine())!=null){
buffer.append(s);
}
System.out.println("服务器收到信息:"+buffer.toString());
// 关闭输入流
socket.shutdownInput();
os = socket.getOutputStream();
os.write("我是服务器发来的信息".getBytes());
os.flush();
socket.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(os!=null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bufferedReader!=null){
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(reader!=null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- step2:
创建TcpServer
package com.genlot.work.tcpippro;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Time:2019/3/26
* Author:Jimmy Wang
* Email:[email protected]
* Blog:https://blog.****.net/wzy901213
* Description:
*/
public class TcpServer {
public static void main(String []args){
try {
ServerSocket serverSocket = new ServerSocket(12888);
while (true){
System.out.println("TcpServer~~~监听~~~");
Socket socket = serverSocket.accept();
ServerThread thread = new ServerThread(socket);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
下面我使用两个手机,多次进行与服务器的连接,演示如下:
总体结果:
TCP 的多线程通信
单独看两个手机
重要的事情说三遍!万丈高楼平地起!万丈高楼平地起!!万丈高楼平地起!!!只有当我们明白了最底层的,知识才是最牢固的。上面的讲解的是基于TCP协议的socket编程。而我们后来将要讲的HTTP相关的大都是基于TCP/IP协议的。一个TCP/IP协议我们又不能直接使用,Socket可以说是TCP/IP协议的抽象与包装,然后我们就可以做相对于TCP/IP的网络通信与信息传输了。
UDP编程
上面我们讲解了基于TCP协议的Socket编程,现在开始我们就开始讲解基于UDP协议的Socket编程了。
UDP,是User Datagram Protocol,也就是用户数据包协议。关键点在于“数据包”。主要就是把数据进行打包然后丢给目标,而不管目标是否接收到数据。主要的流程就是:<u>发送者打包数据(DatagramPacket)然后通过DatagramSocket发送,接收者收到数据包解开数据。</u>
主要API:
DatagramPacket,用来包装发送的数据
构造方法
-
发送数据的构造
DatagramPacket(byte[] buf, int length,SocketAddress address)
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于 buf.length。 -
接收数据的构造:
public DatagramPacket(byte[] buf, int length)
用来接收长度为 length 的数据包。
DatagramSocket:
构造方法
DatagramSocket()
构造数据报套接字并将其绑定到 <u>本地主机上任何可用的端口 </u>。套接字将被绑定到通配符地址,IP 地址由内核来选择。
DatagramSocket(int port)
创建数据报套接字并将其绑定到<u>本地主机上的指定端口</u>。套接字将被绑定到通配符地址,IP 地址由内核来选择。
发送数据
send(DatagramPacket p)
从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
接收数据
receive(DatagramPacket p)
从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。
下面开始代码了
客户端
主要页面与上面的tcp一致,只不过是通讯时的方法改了。如下:
public void udpConnect(View view) {
DatagramPacket packet ;
DatagramSocket socket ;
try {
byte[] bytes = "this is client msg".getBytes();
InetAddress address = InetAddress.getByName("192.168.137.1");
packet = new DatagramPacket(bytes,bytes.length,address,12999);
socket = new DatagramSocket();
socket.send(packet);
byte[] recBytes = new byte[1024];
DatagramPacket recPacket = new DatagramPacket(recBytes,recBytes.length);
socket.receive(recPacket);
Log.e("wzytest","客户端收到消息:"+new String(recBytes, 0, recBytes.length));
} catch (IOException e) {
e.printStackTrace();
}
}
服务端
UDPServer
try {
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes,bytes.length);
DatagramSocket socket = new DatagramSocket(12999);
socket.receive(packet);
System.out.println("服务器收到客户端发来的消息:"+new String(bytes, 0, bytes.length));
DatagramPacket packet1 = new DatagramPacket(bytes,bytes.length,packet.getAddress(),packet.getPort());
socket.send(packet1);
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
UDP通信
TCP与UDP区别与使用场景
至此,基于TCP、UDP协议的Socket通信已经讲完了基础部分。那么这两个协议在实际中有什么区别,分别适用于什么场景呢?
TCP
对于TCP的数据传输而言,传输数据之前需要进行三次握手建立稳定的连接。建立连接通道后,数据包会在这个通道中以字节流的形式进行数据的传输。由于建立稳定连接后才开始传输数据,而同时还是以字节流的形式发送数据,所以发送数据速度较慢,但是不会造成数据包丢失。即使数据包丢失了,会进行数据重发。同时,如果收到的数据包顺序错乱,会进行排序纠正。
三次握手??
这个网络上的解释太多了,想详细了解的自行去百度上Google一下。<u>简单理解</u>的就是这样的:我家是农村的,记得小时后爷爷在田里种地。到了晌午时间,奶奶快烧好饭后我都要去喊爷爷吃饭,因为干农活的地离家里不远不近的,我就跑到隔壁家里的平顶房上喊爷爷吃饭。我先大喊一声“爷爷,回家吃饭啦”。爷爷如果听到我说的话就会给我一个应答“好的!知道了,马上就回去,你们先吃吧!”我只有听到了这句话,才知道爷爷这个时候能听到我说的话,我然后就再次回答爷爷:“好的!那你快点!”这三句话说完,就确定了我能听到爷爷的应答,爷爷也能听到我的回复。这样我就确定我跟爷爷之间的喊话通道是正常的,如果还想对爷爷说什么话,直接说就好了。最后,爷爷听到了我说的话,就不再回复我的话了,然后,拿起锄头回来了。
总结下来,就是面向连接、数据可靠,速度慢,有序的。
<u>适用于需要安全稳定传输数据的场景。例如后面要讲解的HTTP、HTTPS网络协议,FTP文件传输协议以及POP、SMTP邮件传输协议。或者开发交易类、支付类等软件时,都需要基于TCP协议的Socket连接进行安全可靠的数据传输等等</u>
UDP
对于UDP的数据传输而言,UDP不会去建立连接。它不管目的地是否存在,直接将数据发送给目的地,同时不会过问发送的数据是否丢失,到达的数据是否顺序错乱。如果你想处理这些问题的话,需要自己在应用层自行处理。
总结下来,不面向连接、数据不可靠、速度快、无序的。
<u>适用于需要实时性较高不较为关注数据结果的场景,例如:打电话、视频会议、广播电台,等。