Android中使用Socket
Socket简介
在说Socket之前,我们有必要先来简单介绍一下TCP/IP协议族,TCP/IP(Transmission Control Protocol/Internet Protocol)即 传输控制协议/网间协议,定义了主机如何连入因特网及数据如何再它们之间传输的标准,
从字面意思来看TCP/IP是TCP和IP协议的合称,但实际上TCP/IP协议是指因特网整个TCP/IP协议族。不同于ISO模型的七个分层,TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中
应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
传输层:TCP,UDP
网络层:IP,ICMP,OSPF,EIGRP,IGMP
数据链路层:SLIP,CSLIP,PPP,MTU
每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的
注意看我们的传输层,它大致有2个协议,一个是TCP 一个是UDP,他们就与我们的Socket息息相关了.
什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。 如下图
也就是说,我们可以通过直接操作Socket来实现使用TCP或者UDP来发送数据
使用TCP协议来传输数据
TCP协议 是一种面向连接的、可靠的、基于字节流的传输层通信协议,关于它为什么稳定,3次握手和4次挥手的细节就不赘述了,可以自行Google,我们需要知道的是,在使用基于TCP协议的Socket的时候,我们操作的对象是流就可以了
代码
通过一个小例子,我们来看一下在Android中使用的基于TCP协议的Socket通信
服务端
我们在进行Socket通信的时候,是需要两个部分的,一个是服务端,一个是客户端,我们用一段Java代码来模拟服务端,用Android程序来模拟客户端,但是值得注意的是,我们也是可以将Android程序作为服务端的
WebConfig
public class WebConfig {
/**
* 端口
*/
private int port;
/**
* 最大监听数
*/
private int maxParallels;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getMaxParallels() {
return maxParallels;
}
public void setMaxParallels(int maxParallels) {
this.maxParallels = maxParallels;
}
}
我们首先写一个 WebConfig 的类,它用来保存我们的一些设置信息,例如监听的端口号、最大监听数等信息
接下来我们就可以开始写我们的SocketServer的类了
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.lang.System.out;
/**
* Created by ChenFengYao on 16/7/25.
*/
public class SocketServer {
//线程池,为并发考虑
private ExecutorService mThreadPool;
private WebConfig mWebConfig;
private boolean isEnable;
private ServerSocket mServerSocket;
public SocketServer(WebConfig webConfig) {
mWebConfig = webConfig;
mThreadPool = Executors.newCachedThreadPool();
}
/**
* 启动server(异步)
*/
public void startAsync() {
isEnable = true;
new Thread(new Runnable() {
@Override
public void run() {
doProcSycn();
}
}).start();
}
/**
* 停止Server(异步)
*/
public void stopAsync() throws IOException {
if (isEnable) {
return;
}
isEnable = false;
mServerSocket.close();
mServerSocket = null;
}
//异步执行
private void doProcSycn() {
try {
InetSocketAddress socketAddress
= new InetSocketAddress(mWebConfig.getPort());
mServerSocket = new ServerSocket();//创建出ServerSocket对象
mServerSocket.bind(socketAddress);
while (isEnable) {
//ServerSocket的accept方法会让ServerSocket一直等在这行代码,
//直到有远程的Socket对象连入
final Socket remotePeer = mServerSocket.accept();
mThreadPool.execute(new Runnable() {
@Override
public void run() {
String s = null;
try {
s = new String("一个远程连接已连接:".getBytes(), "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
out.println(s + remotePeer.getRemoteSocketAddress().toString());
onAcceptRemotePeer(remotePeer);
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void onAcceptRemotePeer(Socket client) {
try {
//获取Socket的输出流,用来向客户端发送数据
PrintStream out = new PrintStream(client.getOutputStream());
//获取Socket的输入流,用来接收从客户端发送过来的数据
BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));
boolean flag = true;
while (flag) {
//接收从客户端发送过来的数据
String str = buf.readLine();
if (str == null || "".equals(str)) {
System.out.println("没有消息退出");
flag = false;
} else {
if ("bye".equals(str)) {
//如果客户端发送的消息是"bye"就退出程序
flag = false;
} else {
System.out.println("收到消息" + str);
//将接收到的字符串前面加上echo,发送到对应的客户端
out.println("echo:" + str);
}
}
}
out.close();
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个类的代码写的还是比较详细的,主要的方法有doProcSycn,在这个方法里,我们首先对服务端的ServerSocket进行了初始化,之后执行了它的accept()方法,该方法会让线程停在这,直到有一个远程的Socket对象连入,如果连入了,我们就开启一个子线程来专门处理与这个Socket对象通信,具体处理通信的代码在onAcceptRemotePeer()方法里,注意 我们在该方法里,会从远程的Socket对象中获得一个输入流,和一个输出流,输入流里可以取出我们接受的信息,而输出流就是我们想要发送出去的信息,我们在读流的时候是通过一个循环来实现的,没一次读一行,所以,当我们在发送数据的时候,每一次都要在最后加上换行.当我们读取到客户端发送的信息是bye的时候,代表客户端想要关闭socket了,我们就退出循环,关闭socket,其他情况我们都直接回复echo+客户端的信息来模拟回复的消息
基本代码就完成了,我们来一个主函数,来开启服务就好了.
public class Main {
public static void main(String[] args){
WebConfig webConfig = new WebConfig();
webConfig.setPort(8088);
//设置最大连接数
webConfig.setMaxParallels(5);
SocketServer simpleHttpServer;
simpleHttpServer = new SocketServer(webConfig);
simpleHttpServer.startAsync();
}
}
Android端
服务器写完了,我们来看看Android端,我们来一个EditText,一个Button,点击Button将EditText里的信息发送出去,当接到回执的信息后,我们就来一个Toast
布局文件:
<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:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:orientation="vertical"
tools:context="com.lanou.chenfengyao.socketdemo.MainActivity">
<EditText
android:id="@+id/main_et"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/send_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送" />
</LinearLayout>
可以看到布局文件并不是很复杂,就是一个线性布局,里面一个TextView,一个Button
然后看看我们的MainActivity
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import org.json.JSONArray;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
public class MainActivity extends AppCompatActivity {
private Button sendBtn;
private EditText mainEt;
private String host = "192.168.31.228";
private int port = 8088;
private Socket client;
private PrintStream out;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sendBtn = (Button) findViewById(R.id.send_btn);
mainEt = (EditText) findViewById(R.id.main_et);
sendBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String str = mainEt.getText().toString();
//发送数据到服务端
out.println(str);
if("bye".equals(str)){
out.close();
try {
client.close();
client = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
initSocket();
}
private void initSocket() {
new Thread(new Runnable() {
@Override
public void run() {
try {
client = new Socket(host, port);
client.setKeepAlive(true);
//获取Socket的输出流,用来发送数据到服务端
out = new PrintStream(client.getOutputStream());
InputStream inputStream = client.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String s = "";
while ((s = bufferedReader.readLine()) != null) {
Log.d("MainActivity", s);
}
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
代码也并不是很复杂,在onCreate方法里 我们调用initSocket()方法,值得注意的是,使用Socket来进行通信的时候,Socket创建的代码一定要放到子线程中的.然后拿到socket的输出流,并设置为一个全局变量,然后获得输入流,在这个输入流里,循环的读取远程传过来的信息,直到对面的流关闭了,而在点击事件里,每一次点击,都获取EditText中的内容,通过输出流 发送到远端,注意host的地址,必须要填写服务端的地址,由于我是服务端和手机端都是在同一个局域网里的,所以这里就填局域网的ip就可以了.
最后还需要在清单文件里声明网络权限
使用UDP的Socket通信
UDP与TCP的区别在于 UDP并不是一个面向连接的,也就是我们没办法获得输入输出流,而UDP也不是一个可靠的通信,我们没办法保证UDP发送的数据一定会传到,中间不会丢包,丢了我们也不知道,他就是发,不管对面能不能接到,也没有客户端和服务端的概念,但是UDP的优势在于快,直接就发,不需要握手,不需要回执,发就可以了,所以在网络不稳定的时候,或者局域网,即时通讯这些场景下,UDP还是比较常见的.
TCP | UDP |
---|---|
是否面向连接 | 面向连接 |
是否可靠 | 可靠的 |
应用场景 | 传输大量数据 |
速度 | 慢 |
代码
由于也需要2个部分才能进行数据的传递,所有我们这里同样,也是一个使用电脑上的代码,一个运行在Android虚拟机里
电脑端
同样 我们先来写一个电脑端的代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* Created by ChenFengYao on 16/9/30.
*/
public class Main {
public static void main(String[] args) throws IOException {
String strSend = "Hello ANDROID";
byte[] buf = new byte[1024];
//服务端在3000端口监听接收到的数据
DatagramSocket ds = new DatagramSocket(3000);
//接收从客户端发送过来的数据
DatagramPacket dpReceive = new DatagramPacket(buf, 1024);
System.out.println("server is on,waiting for client to send data......");
boolean f = true;
while (f) {
//服务器端接收来自客户端的数据
ds.receive(dpReceive);
System.out.println("server received data from client:");
String strReceive = new String(dpReceive.getData(), 0, dpReceive.getLength()) +
" from " + dpReceive.getAddress().getHostAddress() + ":" + dpReceive.getPort();
System.out.println(strReceive);
//数据发动到客户端的3000端口
DatagramPacket dpSend = new DatagramPacket(strSend.getBytes(), strSend.length(), dpReceive.getAddress(), 3000);
ds.send(dpSend);
//由于dp_receive在接收了数据之后,其内部消息长度值会变为实际接收的消息的字节数,
//所以这里要将dp_receive的内部消息长度重新置为1024
dpReceive.setLength(1024);
}
ds.close();
}
}
注释写的还是比较详细的,可以看到,在使用UDP的时候,我们发送和接收使用的都是DatagramSocket,而无论是发送 还是接收的数据,都是放到DatagramPacket里面的,而数据是要放进byte数组里的,这里发送和接收的端口号是可以不同的,因为双方都可以看成是服务器,所有都需要指定端口
Android
再看看Android端,Android端的代码实际上和电脑端的是非常类似的
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
/**
* Created by ChenFengYao on 16/7/26.
*/
public class UDPAty extends AppCompatActivity {
private Button sendBtn;
private EditText mainEt;
private String mHost = "192.168.43.162";
private int port = 3000;
private DatagramSocket mDatagramSocket;
private InetSocketAddress mSocketAddress;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initUDP();
sendBtn = (Button) findViewById(R.id.send_btn);
mainEt = (EditText) findViewById(R.id.main_et);
sendBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
String recData = mainEt.getText().toString();
//发送数据到
try {
DatagramPacket dpSend = new DatagramPacket(recData.getBytes()
, recData.getBytes().length
, mSocketAddress);
mDatagramSocket.send(dpSend);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
});
}
private void initUDP() {
try {
mDatagramSocket = new DatagramSocket(port);
//每个数据包的大小
new Thread(new Runnable() {
private String mData;
@Override
public void run() {
while (true) {
mSocketAddress = new InetSocketAddress(mHost,port);
try {
//一直等待接收数据
byte[] buffer = new byte[1024];
DatagramPacket receiver = new DatagramPacket(buffer, 1024);
mDatagramSocket.receive(receiver);
mData = new String(receiver.getData(),0,receiver.getLength());
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(UDPAty.this, mData, Toast.LENGTH_SHORT).show();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
} catch (SocketException e) {
e.printStackTrace();
}
}
}
可以看到,同样的 也是需要DatagramSocket 来发送和接收数据的,所以说 对于UDP来说,是不分服务端和客户端的