前言
socket是套接字,是一个对 TCP / IP协议进行封装的编程调用接口,嗯。。。比较官方规范的介绍方式,但如果你刚接触网络编程时,看到这个解释可能会想说,谁他妈知道套接字是个什么鬼。感觉就像用原版的英语词典查一个生词,单词的解释是一句英语,有几个词不认识,得递归查,内心神兽奔腾而过~-~。
简单介绍一下socket相关知识点:
1.协议:
可以先简单理解为约定,比如usb接口,定义了usb的各种标准,包括它的基本外观,尺寸,和其他什么参数,然后所有需要usb接口的产品都按照约定来做,这样子,随便买根数据线,在哪里都能传输数据,充电,正常工作。那么就可以理解,TCP和UDP作为一种网络运输层的协议,对于两个网络设备,如果都实现了该协议,那么在该协议的基础上,设备之间的网路运输层就可以正常交互。
2.Socket和Http:
网络分为5层,最上面两层分别是应用层和运输层,TCP和UDP是实现了运输层的协议,而Socket是对TCP和UDP协议进行了封装,方便上层调用,HTTP属于应用层,他们不在一个层级,本不具备可比性,运输层协议是为了约定数据传输的方式,应用层协议是为了解决数据的包装方式。
举个栗子:
从学校前门到学校后门,我们需要送一千本书过去。
- 这里书就是要传输的数据,前门和后门相当于需要通信的客户端和服务端。
- 出于运输速度和人力资源分配的考虑,我们约定书要分成最多100本一捆的格式去运输比较高效,这一千本书要按顺序分成1~100,101~200。。。901~1000,这样子依次送,这就是运输的协议。
- 那么在学校后门这里,因为我知道从前门送过来的书是每捆100本按顺序依次送过来,那么我把这些书按照收到的顺序依次拼接起来,那么就保证了些书原本的顺序,即数据的完整性,有序性没有被破环。前门运输之前,书是什么样子,后门这里拿到后还是什么样子。
- 而如果我们约定这些书是用纸箱装还是用车拖,学校的后门的地理位置在哪,送到后门后如果没有被及时领走的话,应该保存多久,到期了是扔掉还是怎么处理。这些除数据内容本身以外的约定,Http协议在解决的问题。
- 如果一定要把Socket和Http拿来比较,差别就是在工作方式上,Http通信中只有客户端向服务端请求了数据,服务端才能响应并发数据给客户端,而Socket通信中,只要客户端和服务端之间建立了连接,那么在客户端没有请求数据的情况下,服务端可以主动向客户端发送数据。
3.TCP和UDP
其实在网络编程中,TCP的更为常用一些,这里简单聊一下区别
TCP 3次握手举例:
Client:“Service,我要连”
Service: “好,我知道你要连,同意Client连接”
Client:“哦,我知道你知道我要连”
-
TCP:面向连接、面向字节流、双向通信、 可靠
-
面向连接:TCP在客户端和服务端数据交互之前,有一个3次握手确认连接的过程,只有客户端和服务端都确认了彼此的连接状态,才会开始传输数据。
- 以上,就可以建立连接通道了,这样的好处是避免网络延时等情况下,Client发送连接请求后,Service没收到,而等到Client已经不需要和Service通信后,Service才收到连接请求,如果直接就这样子建立通道了,会造成资源浪费。
- 即然说到连接的3次握手,那就有必要提一下TCP断开连接的4次回收,即任一方发送“我要断开连接的通知”,接收方回复“已经知晓你断开连接了”,4次是因为任一方都可以发送断开连接的消息。
- 面向字节流:流是字符序列。对于TCP而言,传输的报文长度有最大限制,对于更大的数据而言,就必须把该数据分割成一块块的数据块,全部传输完成后,在拼接成原始数据。
- 可靠:按顺序传输数据、不丢失、不重复
-
面向连接:TCP在客户端和服务端数据交互之前,有一个3次握手确认连接的过程,只有客户端和服务端都确认了彼此的连接状态,才会开始传输数据。
-
UDP:无连接的、面向报文、不可靠
- 无连接、不可靠:不需要像TCP那样,建立连接后通讯,它只需要数据要送到哪里去,就可以开始传输数据,至于数据是否丢失,一概不管
- 面向报文:数据有多大,UDP就一次性传输多大,不做切割数据的动作
使用方式:
-
客户端实现
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client_tcp);
ButterKnife.bind(this);
//线程池
pools = Executors.newCachedThreadPool();
//启动服务端
startService(new Intent(this, ServiceTcp.class));
}
这里我们创建一个Activity命名为ClientTcp左为客户端
方便起见,用线程池替代创建线程执行任务
pools.execute(new Runnable() {
@Override
public void run() {
//在连接成功以前,会循环尝试连接知道成功为止
while (mSocket == null || !mSocket.isConnected()) {
try {
//这里构造socket传入ip和端口号,拿到socket对象
mSocket = new Socket("localhost", 2000);
} catch (final IOException e) {
e.printStackTrace();
//toast要切换到主线程中执行
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ClientTcp.this, "连接失败" + "\r\n" + "一秒后重连" + "\r\n" + e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
try {
Thread.sleep(1000); //延时1秒重连
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
try {
//注意,在上面连接成功以后代码才会走到这里,否则在while循环那里是阻塞的状态
br = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
os = mSocket.getOutputStream();//初始化输入输出流
} catch (IOException e) {
e.printStackTrace();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ClientTcp.this, "连接成功", Toast.LENGTH_SHORT).show();
}
});
//死循环接受消息
acceptMessage(br);
}
});
在子线程我,我们尝试连接Service段socket,做了连接失败后重新连接的处理以及输入输出流的初始化,接下来,我们看看acceptMessage(br)方法做了在接收数据时做了怎样的处理
private void acceptMessage(BufferedReader br) {
while (mSocket.isConnected()){
try {
while (!br.ready()){} //当流中没有数据的时候,会一直阻塞在这里
final String response = br.readLine();
runOnUiThread(new Runnable() {
@Override
public void run() {
//将用以标记换行的符号"~~"还原,并切换到主线程中显示
String[] split = response.split("~~");
mShowMessage.setText(split[0] + "\n" + split[1]);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
当socket中获取到的输入流中没有数据时,下面的代码不会执行,一旦有数据,就会调用readLine()方法读出字符,这里做了一个小处理:服务端把接受的详细acceptMessage和要回复的消息responseMessage以acceptMessage + "\n" + responseMessage凭借换行符后发给客户端,但是客户端在执行readLinea()读取数据时,读到"\n"便会停止读取,如果没有读到取"\n",便会一直阻塞 在这里,这也是为什么客户端和服务端在要发送的消息字符串的末尾都会添加"\n"。所以客户端接收到服务端的消息后,将临时定义的“~~”再替换回换行符号,让textview显示的时后能方便区分发送的和接受的消息
case R.id.sendMessage:
String str = mEt.getText().toString();
if (os != null && !TextUtils.isEmpty(str)){
try {
//在字符串末尾拼接换行符,避免服务端socket读取数据时阻塞线程
os.write((str + "\r\n").getBytes());
os.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
break;
发送很简单,需要注意一下flush()和close的区别,close()会关闭掉流,在关闭掉之前会刷新一次流中的数据,那么这个流接下来就不能使用了,而flush刷新后,可以继续使用
-
服务端实现
@Override
public void onDestroy() {
super.onDestroy();
isServideTcpDestory = true;
}
private class TcpAcceptRunnable implements Runnable {
protected ServerSocket mServerSocket;
@Override
public void run() {
try {
mServerSocket = new ServerSocket(2000);
} catch (IOException e) {
e.printStackTrace();
}
while (!isServideTcpDestory) {
try {
//这里是一个阻塞方法,如果没有客户端请求连接,线程会停在这里等待
final Socket socket = mServerSocket.accept();
mThreadPools.execute(new Runnable() {
@Override
public void run() {
try {
responseTcpClient(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务端,只要在服务开启时,执行这个Runnable就可以了,在服务ondestory时,会将isServideTcpDestory值设置为true,从而终止while死循环。值得一提的是,new ServiceSocket(2000)会使该线程监听自己的2000端口,这里的mServerSocket.accept()方法会产生一个socket,但知道有客户端请求连接2000端口位置,线程会一直阻塞在这里。拿到和特定客户端对应的服务端Socket对象后,我们看看在子线程中responseTcpClient(socket)方法作了什么处理
private void responseTcpClient(Socket client) throws IOException {
//操作流
BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
String acceptResponse = "";
while (!isServideTcpDestory) {
while (!br.ready()) { }
acceptResponse = br.readLine();
String responseStr = mStrings[new Random().nextInt(mStrings.length)];
//因为readLine()方法在读到换行符之前会一直等待,这里用"~~"代替换行,在clientTcp中拿到数据再替换成换行符设置给textview
bw.write("sendMessage: " + acceptResponse + "~~" + "receiveResponse: " + responseStr + "\r\n");
bw.flush();
}
br.close();
bw.close();
client.close();
}
看起来,和在客户端的acceptMessage(BufferedReader br)方法中并没有什么差别
我们来看看最后的效果