基于Socket的网络通信简介

使用Socket进行网络通信具有以下特点:Socket基于TCP连接,也就是所谓的长连接,理论上客户端和服务器端在建立连接后并不会主动断掉;而我们前面提到的HTTP通信建立在请求-响应形式上,也就是所谓的短连接,协议本身是无状态的,仅仅是客户端向服务器端发起一次请求,服务器端响应后就意味着连接断开。实际应用中我们使用session/cookie 来解决HTTP无状态的问题。

TCP/IP协议参考模型将所有的TCP/IP类协议归类到四个抽象层中,这四层由底向高分别是:数据链路层、网络层、传输层和应用层。而Socket是在应用层和传输层中间的一个抽象层,并提供了一系列接口提供应用层在通信中使用,Socket通常用于即时通讯等应用场景。

本节我们介绍使用Socket进行简单的通信。使用Socket进行通信可以分为三个步骤:服务器端监听客户端请求连接确认

以Java为例,建立Socket并通过客户端向服务器端发送一条消息的流程如下图:

Java中Socket建立过程

从服务器端开始介绍。

首先在服务器端类的构造函数中实例化 ServerSocket并绑定监听端口(065535之间,通常因为系统占用等原因01024内端口不建议使用):

1
2
3
4
5
6
7
8
9
10
11
Server() {
try {
this.sc = new ServerSocket(PORT);
} catch (IOException e) {
e.printStackTrace();
}
}

然后使用accept阻塞,并用DataOutputStream包裹socket输出流,用DataInputStream包裹socket输入流:

1
2
3
4
DataOutputStream dataOut = new DataOutputStream(soc.getOutputStream());
DataInputStream dataIn = new DataInputStream(soc.getInputStream());

分别启动收取线程和发送线程:

1
2
3
4
new ReceiveThread().start();
new SendThread().start();

现在,服务器端就可以正常启动了。

然后开始介绍客户端。首先新建Android Project,新建空的activity,在布局文件中拖入编辑框、文本框和按钮等控件,并绑定响应的id以供调用。范例布局文件activity_main.xml
为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="cn.edu.chd.SimpleSocket.MainActivity">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:id="@+id/inputIP" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:text="连接服务器"
android:id="@+id/connect"/>
</LinearLayout>
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="0.86" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:id="@+id/textView" />
</ScrollView>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:id="@+id/inputLine" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:text="发送"
android:id="@+id/send"/>
</LinearLayout>
</LinearLayout>

在MainActivity中使用findViewById()绑定布局文件中的控件,给“连接”按钮(R.id.connet) 绑定点击事件方法connect(),给“发送”按钮(R.id.send)绑定点击事件方法send()。因为监听端口收取服务器数据是一个循环等待的操作,如果在主线程中使用会使UI界面卡掉,所以我们在connect()方法中使用AsyncTask来进行异步操作。

1
2
3
4
5
6
7
8
9
10
11
12
AsyncTask<String, String, Void> read = new AsyncTask<String, String, Void>() {
@Override
protected Void doInBackground(String... arg0) {
}
@Override
protected void onProgressUpdate(String... values) {
}
}

我们在后台执行的doInBackground()方法中要进行以下几个操作:

  1. 与服务器端socket连接的建立和对soc流数据的包裹;
  2. 使用readUTF()方法循环读取数据。
    完整的doInBackground()方法为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
protected Void doInBackground(String... arg0) {
try {
socket = new Socket(arg0[0], PORT);
dataIn = new DataInputStream(socket.getInputStream());
dataOut = new DataOutputStream(socket.getOutputStream());
publishProgress("@success");
} catch (UnknownHostException e1) {
Looper.prepare();
Toast.makeText(MainActivity.this, "未知主机,无法建立链接", Toast.LENGTH_SHORT).show();
Looper.loop();
e1.printStackTrace();
} catch (IOException e1) {
Looper.prepare();
Toast.makeText(MainActivity.this, "无法建立链接", Toast.LENGTH_SHORT).show();
Looper.loop();
e1.printStackTrace();
}
try {
String line;
while ((line = dataIn.readUTF()) != null){
publishProgress(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

在onProcessUpdate()方法中进行socket连接成功的提示,并将接收到服务器的通信数据追加到聊天记录框中,完整的onProcessUpdate()方法为:

1
2
3
4
5
6
7
8
9
@Override
protected void onProgressUpdate(String... values) {
if (values[0].equals("@success")) {
Toast.makeText(MainActivity.this, "链接成功!", Toast.LENGTH_SHORT).show();
}
text.append("[Server]:"+values[0]+"\n");
super.onProgressUpdate(values);
}

现在介绍发送数据给服务器的send方法。由于不需要进行后台费时操作,所以我们直接使用writeUTF()方法将数据发送即可,完整的send()方法为:

1
2
3
4
5
6
7
8
9
public void send() {
try {
text.append("[Client]:" + inputText.getText().toString() + "\n");
dataOut.writeUTF(inputText.getText().toString()+"\n");
inputText.setText("");
} catch (IOException e) {
e.printStackTrace();
}
}

运行程序,Android程序运行结果如下:
Android-Socket-client

这里要注意在Android模拟器中访问本地站点(localhost)的IP为10.0.2.2。

服务器端运行结果:
Android-Socket-Server

本节完整的程序见:

https://github.com/dettac/Study-Java-Socket-Together
[lesson6]

0%