前言
手机作为移动设备,很受大家的青睐,因为它携带方便,可以随时随地上网聊天、玩游戏(如现在最火王者某某),这些都是在联网的情况进行的,如果手机不能上网的话,那么它就是没有用的铁疙瘩(某某奇葩也就不会为了某某果机割肾伤身了),因此网络对手机来说有着至关重要的作用。
由于JDK本身集成了TCP、UDP网路协议,那么Android 完全支持它;Android 也可以使用ServerSocket(服务端)、Socket(客户端),而ServerSocket、Socket是基于TCP/IP协议的网络通信;Android也可以使用DatagramSocket、DatagramPacket、MulticastSocket来建立基于UDP协议的网络通信;同时android也支持JDK提供URL、URLConnection;当然android还内置了HttpClient(这个自从android5.0以后需要自己添加Jar包),这样可以非常方便地发送HTTP请求,并获取HTTP响应;但是android并没有内置Web Service的支持,为了弥补这方面的不足,
需要使用第三方类库(KSOAP2)来调用WebService。
一 TCP协议
TCP/IP是一种可靠的网络通信协议,在通信的两端各需要建立一个Socket,从而在两端之间形成虚拟链路,两端的程序可以通过虚拟链路进行通信。如下:
IP协议负责将消息从一个主机传送到另一个主机,信息在传送的过程中被分割成几个小包。
TCP协议负责提供可靠并且无差错的通信服务,它也被称为一种端对端的协议,这是因为,它为两台计算机之间的连接起到重要的作用,当一台计算机需要与另一台计算机连接时,
TCP协议会让它们建立一个连接:用于发送和接收数据的虚拟链路
。
TCP负责收集这些信息包,并将其按适当的次序排好传送,在接收端再将其正确地还原。TCP协议保证了数据包在传送中准确无误。
同时TCP使用重发机制
:当一个通信实体发送一个信息给另一个通信实体后,需要收到另一个通信实体的确认信息,如果没有收到另一个通信实体的确认信息,则会再次重发刚才发送的信息。
综上所述,虽然IP和TCP这两个协议的功能不尽相同,也可以分开单独使用,但它们时在同一个时期作为一个协议来设计的,并且在功能上也是互补的。只有两者结合才能保证Internet在复杂的环境下正常的运行。凡是连接到Internet计算机,都必须安装和使用这两个协议。
二 ServerSocket创建TCP服务端
两个通信实体在进行通信,必须有服务端、客户端之分,而Java中建立服务端是使用ServerSocket,下面看一些它的方法:
ServerSocket(int port):构造函数,参数port是端口,有效值:0~65535。
ServerSocket(int port,int backlog):构造函数,增加一个用来改变连接队列长度的参数backlog。
ServerSocket(int port,int backlog,InetAddress localAddr):构造函数,参数localAddr指定本地IP地址,用于在本地存在过个IP地址的情况。
Socket accept():如果接受到一个客户端Socket的连接请求,该方法将会返回与连接客户端Socket对应的Socket;否则该方法将会一直处于等待状态,线程也被阻塞。
接下来看一个示例:
public static void main(String[] args) {
System.out.print("开启服务!");
try {
//创建一个ServerSocket 对象实例,用来监听客户端Socket的连接状态,端口号为30000,IP为本机IP
ServerSocket ss = new ServerSocket(30000);
//无限循环,不断接收来自客户端Socket的请求,没有Socket请求时会进入阻塞状态。
while(true){
Socket socket = ss.accept();
//获取输出流,向Socket通道中写入数据
OutputStream os = socket.getOutputStream();
os.write("您好,恭喜您中奖了!".getBytes("utf-8"));
//关闭通道
os.close();
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
注意:
上面的输出流
OutputStream
并没有装成PrintStream,然后直接输出,整个字符串,这是由于服务端程序运行于window主机上,当直接使用PrintStream输出整个字符串默认使用系统平台的字符编码GBK,而android是在Linux平台运行的,客户端在读取网络数据时,默认使用的UTF-8编码,这样势必引起乱码。所以为保证能够正确解析数据,要手动控制字符串的编码,强行指定使用UTF-8字符集进行编码。
三 Socket创建TCP客户端
先来看下socket的构造函数:
Socket(InetAddress/String remoteAddress,int port):参数remoteAddress指定远程主机的IP地址,port指定远程主机的端口,这里没有指定本地IP地址,本地端口,默认使用本机的IP地址,默认使用系统动态分配的端口。
Socket(InetAddress/String remoteAddress,int port,InetAddress localAddr, int localPort):增加参数localAddr指定本地Ip地址,localPort指定本地端口,这种情况使用本地主机有多个IP地址的情形。
再看其他重要的方法:
InputStream getInputStream():通过Socket对象实例获取输入流,让程序可以从Socket通道中获取数据。
OutputStream getOutputStream():通过Socket对象实例获取输出流,让程序可以往Socket通道中写入数据。
示例:
new Thread(new Runnable() {
@Override
public void run() {
try {
//建立连接到远程服务器的socket
Socket socket = new Socket("192.168.11.139",30000);
//=================或者==============
//创建Socket对象
Socket s = new Socket();
//连接远程服务
s.connect(new InetSocketAddress("192.168.11.139",30000));
//设置超时时间
socket.setSoTimeout(10000);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//进行I/O 操作
final String result = bufferedReader.readLine();
if (!TextUtils.isEmpty(result)){
ServerSocketActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
txt_serverSocket.setText(result);
}
});
}
//关闭输入流、socket
bufferedReader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
说明,到这里服务端、客户端通信的虚拟链路算是建立起来了,要实现通信客户端还要添加权限:
<uses-permission
android
:name=
"android.permission.INTERNET"
/>
接下来先运行服务器,再运行客户端,那么客户端会收到字符串
"您好,恭喜您中奖了!"
。
四 加入多线程
在上面示例中已经简单的叙述了Socket的通信的过程,上面的示例中是一对一的关系,就是一个服务器对应一个客户端,那么在实际使用中却不是这样的,一个服务器需要服务多个对象(客户端),且可能需要与每个客户端保持长时间的通信,即服务端不断读取客户端的数据,并向客户端写入数据,而客户端也是如此。
当使用
readLine()方法读取数据,该方法成功返回之前,线程被阻塞,程序是无法进行下去
,也就不能接受其他的Socket连接请求了,所以为了解决这个问题,服务端为每个Socket启动一个新的线程,该线程负责与客户端进行通信,这样就不影响服务器接受其他线程了。
服务端:
首先定义一个类继承Runnable(例如我的类名叫作ServerThread),通过构造函数把Socket对象实例传递来,这样就可以获取输入流、输出流了,如:
public ServerThread(Socket s) {
this.s = s;
if (null != s) {
try {
br = new BufferedReader(new InputStreamReader(s.getInputStream(),"utf-8"));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
再者读取Socket中的数据如下:
/**
* 读取客户端数据
* @return
*/
public String readFromClient () {
if(null != br) {
try {
String result = br.readLine();
System.out.println("服务端读取:" +result);
return result;
} catch (IOException e) {
System.out.println("读取失败!");
//捕捉到异常,表明s 对应的客户端已经关闭
MutiThreadServerSocket.socketList.remove(s);
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
还可以把刚刚获取的数据转发给所有的客户端
String content = null;
// TODO Auto-generated method stub
while((content = readFromClient()) != null) {
//MutiThreadServerSocket.socketList为ArrayList<Socket>类型用来记录每个客户端
for(Socket s : MutiThreadServerSocket.socketList) {
try {
OutputStream os = s.getOutputStream();
os.write(content.getBytes("utf-8"));
os.flush();
// os.close();
System.out.println("服务端写入content:" + content);
} catch (IOException e) {
// TODO Auto-generated catch block
System.out.println("服务端写入失败!");
e.printStackTrace();
}
}
}
完整代码
。
最后就是服务程序:
//存储Socket对象
public static ArrayList<Socket> socketList = new ArrayList<>();
public static void main(String[] args) {
System.out.println("开启多线程服务!");
try {
ServerSocket ss = new ServerSocket(30010);
while(true){
Socket socket = ss.accept();
socketList.add(socket);
//每当一个客户端连接之后,就启动一个线程
new Thread(new ServerThread(socket)).start();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
完整代码
。
客户端:
首先定义一个类继承Runnable,如下:
public class ClientThread implements Runnable {
private Handler handler;
private Socket socket;
//输入流 读取服务端传送过来的消息
private BufferedReader br;
//输出流往服务器发送信息
private static OutputStream outputStream;
public static Handler revHandler ;
private InputStream inputStream;
//计时器
private Timer timer = new Timer();
public ClientThread(Handler handler) {
this.handler = handler;
}
@Override
public void run() {
try {
//192.168.11.139 为本地电脑的ip
socket = new Socket("192.168.11.139",30010);
// socket.setKeepAlive(true);
inputStream = socket.getInputStream();
br = new BufferedReader(new InputStreamReader(inputStream));
outputStream = socket.getOutputStream();
Log.e("进入循环线程:","-------------->");
/**
*
* 开子线程读取数据,因为readLine()会导致阻塞
* 对于socket,不能认为把某次写入到流中的数据读取完了就算流结尾了,
* 但是socket流还存在,还可以继续往里面写入数据然后再读取。
* 这时用BufferedReader封装socket的输入流,调用BufferedReader的readLine方法是不会返回null的
* 所以在循环内如果不判断 content!=null && content.length() > 0 那么程序将会一直阻塞在这里(程序是因为readLine阻塞,并不是死循环)
*
*/
new Thread(new Runnable() {
@Override
public void run() {
Log.e("开始读取:","1111");
String content = null;
try {
while (((content = br.readLine()) != null) && (content.length() > 0)){
Log.e("读取====》",content);
//每当有消息时,及时通知主界面更新消息
Message message = new Message();
message.what = 0x123;
message.obj = content;
handler.sendMessage(message);
}
} catch (IOException e) {
closeSocket();
e.printStackTrace();
}
}
}).start();
Looper.prepare();
revHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 0x345){
try {
outputStream.write((msg.obj.toString() + "\r\n").getBytes("utf-8"));
Log.e("写入====》",msg.obj.toString());
outputStream.flush();
} catch (UnsupportedEncodingException e) {
closeSocket();
e.printStackTrace();
} catch (IOException e) {
closeSocket();
e.printStackTrace();
}
}
}
};
//把当前的线程初始化为looper//,循环读取服务端发过来的消息
Looper.loop();
} catch (IOException e) {
closeSocket();
e.printStackTrace();
}
}
/**
* 关闭端口
*/
private void closeSocket(){
try {
if (null != inputStream){
inputStream.close();
}
if (null != outputStream){
outputStream.close();
}
if (null != br){
br.close();
}
if (null != socket){
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后在主界面添加一个输入框和发送按钮,输入内容后,点击发送把内容发送给服务端,布局文件如下:
<?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="com.ecric.http.socket.MultiThreadClientActivity">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/ed_multi_input"
android:layout_width="0dp"
android:layout_weight="3"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_send"
android:text="发送"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" />
</LinearLayout>
<!--用来展示文本信息-->
<TextView
android:id="@+id/txt_show"
android:text="展示文本信息:"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
最后就是客户端程序:
@EActivity(R.layout.activity_multi_thread_client)
public class MultiThreadClientActivity extends AppCompatActivity {
// 输入信息框
@ViewById(R.id.ed_multi_input)
EditText edInput;
//展示信息
@ViewById(R.id.txt_show)
TextView txtShow;
public Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//如果消息来自于子线程
if (msg.what == 0x123){
txtShow.append("\n" + msg.obj.toString());
}
}
};
//客户端处理数据的线程
private ClientThread clientThread;
@AfterViews
public void initData(){
clientThread = new ClientThread(handler);
new Thread(clientThread).start();
// ClientBody clientBody = new ClientBody("192.168.11.139",30010,"1");
// clientBody.start();
}
/**
* 发送监听
* @param view
*/
@Click(R.id.btn_send)
public void onClick(View view){
//当用户按下发送按钮后,将用户输入的数据,封装成Message
//然后发送给子线程的handle对象
Message message = new Message();
message.what = 0x345;
message.obj = edInput.getText().toString();
clientThread.revHandler.sendMessage(message);
edInput.setText("");
}
}
说明在这个程序也没有处理多少,主要是启动ClientThread,及与输入操作,当用户点击“发送”按钮之后,程序将会吧输入的内容发送给ClientThread的revHandle,进而把内容写到Socket中,传送为服务端;同样从服务读取到内容时,将会把读取到的信息通过handle发送给UI线程,进而更新内容;还有一点要说说明下,上述代码我使用AndroidAnnotations注解框架,使代码看起来简单很多,不感兴趣的可以不管它,查看控件时,老老实实findViewById就可以了。
接下来先运行服务端,再运行客
户端,在输入框输入
QWERTY
点击发送,服务端打印结果:
开启多线程服务!
服务端读取:QWERTY
服务端写入content:QWERTY
客户端结果:
但在此过程中遇到一个问题:服务端可以很好的接受来自客户端数据,但为什么只有服务端进程关闭(或者说在服务端关闭socket)后,客户端才会收到服务端传送过来的数据呢?谁有解决之法、或知其原理务必指点一下小编。
参考文献:
《疯狂android讲义》