在使用Socket实现进程间通信前,先对网络协议相关知识进行简单回顾。
网络分层
一般情况会将网络分为5层:
- 应用层 常见协议:HTTP、FTP、POP3等
- 传输层 常见协议:TCP、UDP
- 网络层 常见协议:IP
- 数据链路层
- 物理层
TCP、UDP
- TCP:面向连接的、可靠的流协议,提供可靠的通信传输。
- 所谓流,就是指不间断的数据结构,你可以把它想象成排水管道中的水流。当应用程序采用TCP发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。
- 有顺序控制、丢包重发机制
- UDP:面向无连接的,具有不可靠性的数据报协议。(让广播和细节控制交给应用的通信传输)
- 无顺序控制、丢包重发机制
TCP用于在传输层有必要实现可靠传输的情况,由于它是面向连接并具有“顺序控制”、重发控制等机制;而UDP则主要用于那些对高速传输和实时性有较高要求的通信或广播通信。
因此TCP和UDP应该根据应用的目的按需使用,没有绝对优缺点。
TCP三次握手与四次挥手
使用TCP协议的连接建立与断开,正常过程下至少需要发送7个包才能完成,就是我们常说的三次握手,四次挥手。
标志位Flags、序号
- 序列号 Sequeuece number(seq): 数据包本身的序列号,初始序列号是随机的。
- 确认号 Acknowledgment number(ack): 在接收端,用来通知发送端数据成功接收
- 标志位,标志位只有为 1 的时候才有效
- SYN(synchronize):表示在连接建立时用来同步序号。
- ACK:TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1.
- FIN(finish):用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。
-
三次握手
三次握手:是指建立一个TCP连接时需要客户端和服务端总共发送3个包确认连接的建立。在Socket编程中,这一个过程由客户端执行connect来触发。
- 第一次握手:客户端向服务端发送请求报文;即SYN=1,ACK=0,seq=x。
- 第二次握手:服务端收到客户端的请求报文,服务端会确认应答,告诉客户端已经收到请求了;即SYN=1,ACK=1,seq=y,ack=x+1;
- 第三次握手:客户端收到服务端的确认应答后,再次向服务端进行确认应答,建立完整的连接;即ACK=1,seq=x+1,ack=y+1
为什么要进行三次握手呢,或两次确认??
下面使用Wireshark抓包工具体验下三次握手的过程
红色框内就是一个TCP建立连接的过程
- 53324 —>80:嘿,哥们,我想访问你的web资源,能不能把你的80端口打开
- 80 —> 53324:可以啊,我已经把80端口打开了,为了保证我们的数据能可靠传输,你那边也需要把53324端口打开;
- 53324 —> 80:没问题,我已经把53324端口打开了,尽管的发送数据过来吧。
下面看看在三次握手的标志位的变化
1、53324 —>80
2、80 —> 53324
3、53324 —> 80
四次挥手
四次挥手:即终止TCP连接,就是断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在Socket编程中,这一工程由客户端或服务端任意一方执行close来触发。
由于 TCP 连接是全双工的,因此每个方向都必须单独进行关闭。这一原则是当一方数据发送完成,发送一个标志位为 FIN 的报文来终止这一方向的连接,收到标志位为 FIN 的报文意味着这一方向上不会再收到数据了。但是在 TCP 连接上仍然能够发送数据,直到这一方向也发送了 FIN 。发送 FIN 的一方执行主动关闭,另一方则执行被动关闭。
- 第一次挥手:客户端发送一个FIN=1,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说”我客户端没有数据要发给你了”,但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
- 第二次挥手:服务器端收到FIN后,先发送ack=u+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。
- 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=1报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
- 第四次挥手:客户端收到FIN=1报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=w+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手。
Socket
socket:
- 位于传输层,是网络上的两个程序通过一个
双向的通信连接
实现数据的交换的一种进程通信方式之一。 - 成对出现,一对套接字
Socket socket = new Socket("localhost", 8888);
localhost:IP地址
8888:端口号
即
IP地址 -- 端口号成对出现
socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供了网络开发所用的接口;HTTP是轿车,提供了封装或显示数据的具体形式;socket是发动机,提供了网络通信的能力。
典型的应用就是C/S结构:
从图可知,socket的使用是基于TCP或UDP协议。
Socket java简单实现
我们知道socket是基于TCP
或UDP
协议实现的,下面以TCP
协议为例实现,因为TCP
更加常用些。
使用步骤
- 客户端
- 创建
socket
对象,指定成对的服务端IP地址和端口号 - 通过
socket
获取输出流,写入数据发给服务端 - 通过
socket
获取输入流,接受服务端的发送的数据 - 关闭资源
close
- 创建
- 服务端(与服务端类似)
- 创建
ServerSocket
对象,并指定端口号,其端口号必须与客服端一致 - 通过
ServerSocket
对象,获取客户端的socket
实例(ServerSocket.accept方法
) - 通过
socket
获取输入流,接受客户端发来的消息 - 通过
socket
获取输出流,写入数据向客户端发送数据作为回应 - 关闭资源
close
- 创建
具体实例
客户端Client
public class Client {
private static final String TAG = "Client";
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
//IO操作不能放在主线程执行
connectServer();
}
}).start();
}
private static void connectServer() {
try {
//1、创建客户端socket,指定服务端地址和端口
Socket socket = new Socket("localhost", 8888);
boolean connected = socket.isConnected(); //检查客户端与服务端是否连接成功
System.out.println(connected?"连接成功":"连接失败,请重试!");
//2、获取输出流,向服务器发送消息
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream),true);
writer.write("第一次来到广州\n");
writer.flush();
//3、获取输入流,并读取服务端的响应信息
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String info=reader.readLine();
System.out.println("客户端收到服务端回应:"+info);
//4、关闭资源
outputStream.close();
writer.close();
inputStream.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端Server
public class Server {
private static final String TAG = "Server";
public static void main(String[] args) {
try {
//1、创建ServerSocket对象,指定与客户端一样的端口号
ServerSocket serverSocket = new ServerSocket(8888);
//2、获取Socket实例
final Socket socket = serverSocket.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
//3、获取输入流,接受客户端发来的消息
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader reader = new BufferedReader(inputStreamReader);
String info=reader.readLine();
System.out.println("服务端收到客户端的信息: " +info);
//4、获取输出流,向客户端发送消息回应
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream);
writer.write("羊城欢迎你!"+"\n");
writer.flush();
//4、关闭IO资源
inputStream.close();
reader.close();
outputStream.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
注意:
使用write方法
写入数据,字符串必须在中间或者末尾添加转义符\r
或者\n
,否则在使用readLine
方法读取数据时,会一直阻塞从而读取失败。如下:
writer.write("第一次来到广州\n");
运行结果
首先运行服务端,再接着运行客户端
服务端Server:
客户端Client
Socket Android使用
既然scoket能够实现两个程序间的信息传输,很明显在Android下是一种IPC方式。
下面以一个简单跨进程聊天程序为例,功能点能够自动回复。
实现流程
- 创建一个远程Service服务,在其建立TCP服务(
服务端
) - 在界面上(Activity、Fragment等)连接TCP服务(
客户端
) - 在Mainfest中声明网络权限及注册Service
具体实现
服务端:
public class TCPServerService extends IntentService {
private static final String[] defaultMessages = {
"你好啊,嘻嘻",
"看了你相片,你好帅哦,很喜欢你这样的",
"我是江西人,你呢?",
"你在哪里工作?"};
private int index = 0;
private boolean isServiceDestroy = false;
//需注意,必须传入参数
public TCPServerService() {
super("TCP");
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
try {
//1、监听本地端口号
ServerSocket serverSocket = new ServerSocket(8954);
Socket socket = serverSocket.accept();
//2、获取输入流,接受用户发来的消息(Activity)
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
//3、获取输出流,向客户端(Activity)回复消息
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream));
//4、通过循环不断读取客户端发来的消息 ,并发送
while (!isServiceDestroy) {
String readLine = reader.readLine();
if (!TextUtils.isEmpty(readLine)) {
String sendMag = index < defaultMessages.length ? defaultMessages[index] : "已离线";
SystemClock.sleep(500); //延迟发送
writer.println(sendMag+"\r"); // `\r或\n`必须要有,否则会影响客户端接受消息
writer.flush(); //刷新流
index++;
}
}
//关闭流
inputStream.close();
reader.close();
outputStream.close();
writer.close();
socket.close();
//需关闭,否则再次连接时,会报端口号已被使用
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onDestroy() {
super.onDestroy();
isServiceDestroy = true;
Log.d(TAG, "onDestroy: ");
}
private static final String TAG = "TCPServerService";
}
客户端:
public class SocketActivity extends AppCompatActivity {
private TextView mTvChatContent;
private EditText mEtSendContent;
private Intent mIntent;
private static final int CONNECT_SERVER_SUCCESS = 0; //与服务端连接成功
private static final int MESSAGE_RECEIVE_SUCCESS = 1; //接受到服务端的消息
private static final int MESSAGE_SEND_SUCCESS=2; //消息发送
@SuppressLint("all")
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case CONNECT_SERVER_SUCCESS:
//与服务端连接成功
mTvChatContent.setText("与聊天室连接成功\n");
break;
case MESSAGE_RECEIVE_SUCCESS:
String msgContent = mTvChatContent.getText().toString();
mTvChatContent.setText(msgContent+msg.obj.toString()+"\n");
break;
case MESSAGE_SEND_SUCCESS:
mEtSendContent.setText("");
mTvChatContent.setText(mTvChatContent.getText().toString()+msg.obj.toString()+"\n");
break;
}
return false;
}
});
private PrintWriter mPrintWriter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_socket);
mTvChatContent = findViewById(R.id.tv_chat_content);
mEtSendContent = findViewById(R.id.et_send_content);
//启动服务
mIntent = new Intent(this, TCPServerService.class);
startService(mIntent);
new Thread(new Runnable() {
@Override
public void run() {
//连接服务端,实现通信交互
//IO操作必须放在子线程执行
connectTCPServer();
}
}).start();
}
private Socket mSocket=null;
private void connectTCPServer() {
//通过循环来判断Socket是否有被创建,若没有则会每隔1s尝试创建,目的是保证客户端与服务端能够连接
while (mSocket == null) {
try {
//创建Socket对象,指定IP地址和端口号
mSocket = new Socket("localhost", 8954);
mPrintWriter = new PrintWriter(new OutputStreamWriter(mSocket.getOutputStream()),true);
if (mSocket.isConnected()) //判断是否连接成功
mHandler.sendEmptyMessage(CONNECT_SERVER_SUCCESS);
} catch (IOException e) {
e.printStackTrace();
//设计休眠机制,每次重试的间隔时间为1s
SystemClock.sleep(1000);
}
}
//通过循环来,不断的接受服务端发来的消息
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
while (!SocketActivity.this.isFinishing()){ //当Activity销毁后将不接受
String msg = reader.readLine();
if (!TextUtils.isEmpty(msg)){
//发消息通知更新UI
mHandler.obtainMessage(MESSAGE_RECEIVE_SUCCESS,msg).sendToTarget();
}
}
//关闭流
mPrintWriter.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@SuppressLint("SetTextI18n")
public void onClick(View view) {
switch (view.getId()) {
case R.id.but_send:
//必须开启子线程,不能在UI线程操作网络
new Thread(new Runnable() {
@Override
public void run() {
String msg = mEtSendContent.getText().toString();
if (mPrintWriter!=null && !TextUtils.isEmpty(msg)){
mPrintWriter.println(msg+"\n");
//此处可以不用刷新流的方法,因为在创建mPrintWriter对象时,在其构造方法中设置了自动刷新缓存
// mPrintWriter.flush();
//通知更新UI
mHandler.obtainMessage(MESSAGE_SEND_SUCCESS,msg).sendToTarget();
}
}
}).start();
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//关闭输入流和连接
if (mSocket!=null){
try {
mSocket.shutdownInput();
mSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//停止后台服务
stopService(mIntent);
}
private static final String TAG = "TCPServerService";
}
Mainfest:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<service android:name=".socket.TCPServerService"
android:exported="true"
android:process=":socket"/>
运行结果:
注意事项:
1、在使用IntentService时,必须在重写其构造方法并指定线程名,否则报错has no zero argument constructor
如下:
public TCPServerService( ) {
super("TCP");
}
2、连接服务端和发送消息必须放在子线程
中执行,否则会报NetworkOnMainThreadException
3、在writer.println()
或writer.write()
方法中传入的字符串必须要有\r
或\n
转义符,同时需刷新流flush()
,否则会影响消息及时性
4、服务端在停止之前必须关闭ServerSocket
,调用close()
即可,否则再次连接时,会报端口号已被使用
错误,java.net.BindException: bind failed: EADDRINUSE (Address already in use)
5、客户端Socket与服务端ServerSocket的端口号port必须一致
6、在Mainfest中必须声明网络权限,否则连接失败,提示没有权限socket failed: EACCES (Permission denied)
以上几点是在开发中容易入坑的地方。
参考
- 安卓开发艺术探索
- https://blog.csdn.net/carson_ho/article/details/53366856
- https://www.jianshu.com/p/9f3e879a4c9c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation
- https://www.cnblogs.com/edisonchou/p/5987827.html
- https://www.cnblogs.com/liyiran/p/9102791.html
- https://blog.csdn.net/oney139/article/details/8103223
- https://baike.baidu.com/item/socket/281150?fr=aladdin