TCP和UDP都属于TCP/IP参考模型中传输层的协议,且都是基于网际互联层IP协议。
一位大神作了一个很形象的比喻:TCP和UDP使用IP协议从一个网络传送数据包到另一个网络。把IP想像成一种高速公路,它允许其它协议在上面行驶并找到到其它电脑的出口。TCP和UDP是高速公路上的“卡车”,它们携带的货物就是像HTTP,文件传输协议FTP这样的协议等。(参考:http://blog.csdn.net/magister_feng/article/details/8634518)
以下是些简单的区别,详细请参考其他资料。
Tables | TCP | UDP |
---|---|---|
名称 | Transmission Control Protocol,传输控制协议 | User Datagram Protocol,用户数据报协议 |
连接方式 | 面向连接(收发数据前,必须与对方建立可靠连接) | 面向非连接 |
握手 | 3次 | 无 |
可靠性 | 可靠 | 不可靠 |
效率 | 慢 | 快 |
应用 | 可靠性高、数据量大的场合 | 快速、数据量小的场合 |
代码这东西,跟生活一样,生活怎么来,代码就怎么写。
TCP、UDP就像生活中与别人通信,需要知道对方的地址和姓名。地址就是对方的IP,收件人就是对方的端口。
发送方:把信件装好,根据地址和收件人给对方送信。
收件方:在家等着收信,根据信中内容作出相应动作。
在程序中,把发送方和收件方分别封装起来,叫Socket。Socket连接必须确保有一对Socket,一个是发送方,另一个是收件方。
不比喻了,不然要被自己套进去。。。
在编程时,注意以下几点:
通信的时候,总要一方在等待收件,即处于端口监听状态,此方成为服务器。TCP的Socket分为ServerSocket(服务器)和Socket(客户端),ServerSocket一直处于端口监听,以便客户端可以随时连接进来。而Socket就是请求连接。然后双方通信。
通信的方式,一般都是先一问一答式,然后双方协定先后顺序发送接收数据。即首先客户端请求,服务器响应,再按协定相互通信。
这样协定,是因为有些方法是阻塞式,但为了更加人性化,一般都会设置超时时间。
常用API:
这里是一个简单的服务器实例,只实现一次请求,然后响应一次即完毕:
private void startTCPServer() {
final int port = 8989;
new Thread(new Runnable() {
@Override
public void run() {
ServerSocket server = null;
try {
// 1、创建ServerSocket服务器套接字
server = new ServerSocket(port);
// 设置连接超时时间,不设置,则是一直阻塞等待
server.setSoTimeout(8000);
// 2、等待被连接。在连接超时时间内连接有效,超时则抛异常,
Socket client = server.accept();
logD("connected...");
// 设置读取流的超时时间,不设置,则是一直阻塞读取
client.setSoTimeout(5000);
// 3、获取输入流和输出流
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
// 4、读取数据
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String receData = new String(buf, 0, len, Charset.forName("UTF-8"));
logD("received data from client: " + receData);
// 5、发送响应数据
byte[] responseBuf = "Hi, I am Server".getBytes(Charset.forName("UTF-8"));
outputStream.write(responseBuf, 0, responseBuf.length);
} catch (IOException e) {
logD("Exception:" + e.toString());
e.printStackTrace();
} finally {
if (server != null) {
try {
server.close();
server = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
常用API:
客户端代码也是最简单的实例,一发一收即结束。
private void startTCPClient() {
final String host = "192.168.1.214";
final int port = 8989;
new Thread(new Runnable() {
@Override
public void run() {
Socket socket = null;
try {
// 1、创建连接
socket = new Socket(host, port);
if (socket.isConnected()) {
logD("connect to Server success");
}
// 2、设置读流的超时时间
socket.setSoTimeout(8000);
// 3、获取输出流与输入流
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
// 4、发送信息
byte[] sendData = "Hello, I am client".getBytes(Charset.forName("UTF-8"));
outputStream.write(sendData, 0, sendData.length);
outputStream.flush();
// 5、接收信息
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
String receData = new String(buf, 0, len, Charset.forName("UTF-8"));
logD(receData);
} catch (IOException e) {
e.printStackTrace();
logD(e.toString());
} finally {
if (socket != null) {
try {
socket.close();
socket = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
因为上面的demo中,都是假设对方发送的数据长度小于等于1024。可是现实中数据的长度一般都是未知的,很多人用读取文件的方法来读取,如:
byte[] buf = new byte[1024];
StringBuilder rece = new StringBuilder();
int len;
while ((len = inputStream.read(buf)) != -1) {
String part = new String(buf, 0, len);
rece.append(part);
}
String receData = rece.toString();
这样,看上去很好,但实际socket的流与文件流不同,文件读到末尾,有一个结束标记。而socket的流只有在关闭流的时候,才会有被告知结束。这里的结束就是read()返回的长度等于-1。
TCP通信时,一般是不会立马关闭,而read()又是阻塞式方法,所以会导致程序一直停留在这里while ((len = inputStream.read(buf)) != -1)
。
首先,有两种不太靠谱的解决方法:
1. 不要用-1作为判断条件,而用>0。但我这结果都一样,不管你信不信,反正我信了
2. 使用BufferedReader的readLine()去读取数据。这是读行,我的数据里没有换行,怎么读?传文件很好,但绝大多数情况下不适合。
BufferedReader bufReader = new BufferedReader(new InputStreamReader(inputStream));
String receData = bufReader.readLine();
稍微靠谱点的方法:
使用while(inputStream.available() > 0),先进行判断是否有数据可读,有就在循环内部读取。这个方法不是阻塞式,如果对方已经把数据发送出来了,这时可以正常读取;如果对方慢了一点,这里就直接执行下去了,数据的读取就被跳过了。当然也可以每次都先等待一段时间再去查。
目前没有找到特别完美又通用的解决方法(如果您有,麻烦分享下),但总有适合的,以下是个人总结的几种解决方法:
1. 制定协议,添加报文头:报文总数据长度 + 是否继续保持链接 + 当前报文的序号,本报文就根据此长度来判断是否接受完毕;
2. 设置读入超时时间。如果后面的读取后面的程序还要执行,那就只对读取进行异常处理;
3. 发送端发送完毕后关闭连接。socket.shutdownOutput()和socket.close()都可以,前者是半关闭,后者是全关闭。前者安全点;
4. 读取到某些字符后,当成结束。如读到byebye就不读了;
5. 让每次传输的数据长度都不等于缓冲区buffer大小的整数倍。接收到数据后,判断长度是否为buffer长度。是,则说明还有数据要读;否,则说明已结束;
6. 用一个足够大的缓冲区buffer来一次性装完所有的数据。
个人还是喜欢用byte,可能因为以前使用C的原因,再加上项目中也是字节数据+字符数据。很多情况下,我们都是字符串数据或对象实例之类的,可以用封装类进行处理,如BufferedWriter/BufferedReader、ObjectInputStream/ObjectOutputStream、DataInputStream/DataOutputStream与PrintWriter。
对象流就不写demo了,需注意的是:
- 被读写的对象必须实现Serializable;
- 读写顺序必须一致。
DataInputStream+DataOutputStream:
// 发送
DataOutputStream dataOS = new DataOutputStream(outputStream);
dataOS.writeUTF("Hi你好啊, I am Server");
dataOS.flush();
dataOS.close(); // 如果发送后不再接收或发送,就可以关闭,否则不要关闭。因为这样也会把socket关闭,导致无法再接收或发送了,并抛SocketException异常。若不再发送,只接收,可用socket.shutdownOutput();
// 接收
DataInputStream dataIS = new DataInputStream(inputStream);
String receData = dataIS.readUTF();
logD("From Client:" + receData);
PrintWriter + BufferedReader:
// 发送
// 使用带自动flush刷新的构造函数,自动刷新仅对这三个方法有效:println, printf, format
PrintWriter printWriter = new PrintWriter(outputStream, true);
printWriter.format("First info from client = %s", host);
printWriter.printf("Second info: Android");
socket.shutdownOutput();
// 接收
// 这里可以一次性把上面两次的写入流读出来。
BufferedReader bufReader = new BufferedReader(new InputStreamReader(inputStream));
char[] buffer = new char[1024];
int length;
if ((length = bufReader.read(buffer)) != -1) {
String receData = new String(buffer, 0, length);
logD("From Client:" + receData);
}
心跳是双方约定好保活时间,在此保活时间内告诉对方自己还在线,不要关闭连接。对方在保活时间内没有收到保活信息,就会关闭连接。
正常情况下,TCP有默认的保活时间,为2小时,可在客户端开启保活功能socket.setKeepAlive(true);。
但我们并不需要那么长的保活时间,一般10分钟就够了。方法有两个:
这里的自由通信,指的是服务器或客户端可以任意的发送和接收数据,而不需按一发一收,想发就发,任意时刻都能接收数据那种。
发送用的是OutputStream,接收用的是InputStream。一发一收制,发送和接收的都是顺序写到代码中的。现在要自由,无非就是分开来,发送只管发送,接收只管接收。
发送,好处理,需要发送的时候发送就行。
接收,好像没那么容易,因为对方无论何时发送数据,我们必须接收。这里开了个子线程在循环读流。又由于读流是阻塞式,使用的处理方法是上面的第2种,设置读流超时时间。
代码包含服务器类SimpleTCPServer与客户端类SimpleTCPClient,及各自的demo。这两个类可以当做简单的工具类使用。
两个工具类对接收的数据使用了不同的处理方法,SimpleTCPServer要求传入Handler进行处理。而SimpleTCPClient是要求调用者实现其抽象方法processData(byte[] data),此方法是在子线程中处理,把更多的处理权交给了调用者。
SimpleTCPServer:
public class SimpleTCPServer {
private static final String TAG = SimpleTCPServer.class.getSimpleName();
private static final int INPUT_STREAM_READ_TIMEOUT = 300;
/**
* 服务器,连接服务器的客户端
*/
private ServerSocket mServer;
private List mClientList = new ArrayList<>();
private Handler mHandler;
public SimpleTCPServer(Handler handler) {
this.mHandler = handler;
}
public void listen(final int port) {
if (mServer != null && !mServer.isClosed()) {
close();
}
new Thread(new Runnable() {
@Override
public void run() {
try {
mServer = new ServerSocket(port);
while (mServer != null && !mServer.isClosed()) {
logI("start to accept");
Socket client = mServer.accept();
if (client.isConnected()) {
logI(String.format("accepted from: %s[%d]", client.getInetAddress().getHostAddress(), client.getPort()));
mClientList.add(client);
new Thread(new ReceiveRunnable(mClientList.size() - 1, client)).start();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 接收客户端数据的子线程
*/
class ReceiveRunnable implements Runnable {
private int which;
private Socket mSocket;
ReceiveRunnable(int which, Socket socket) {
this.which = which;
this.mSocket = socket;
}
@Override
public void run() {
try {
// 给读取流设置超时时间,否则会一直在read()那阻塞
mSocket.setSoTimeout(INPUT_STREAM_READ_TIMEOUT);
InputStream in = mSocket.getInputStream();
while (mSocket != null && mSocket.isConnected()) {
// 读取流
byte[] data = new byte[0];
byte[] buf = new byte[1024];
int len;
try {
while ((len = in.read(buf)) != -1) {
byte[] temp = new byte[data.length + len];
System.arraycopy(data, 0, temp, 0, data.length);
System.arraycopy(buf, 0, temp, data.length, len);
data = temp;
}
} catch (SocketTimeoutException stExp) {
// 只catch,不做任何处理
// stExp.printStackTrace();
}
// 处理流
if (data.length != 0) {
pushMsgToHandler(which, data);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 推送信息给Handler
*/
private void pushMsgToHandler(int which, byte[] data) {
Message message = mHandler.obtainMessage();
message.what = which;
message.obj = data;
mHandler.sendMessage(message);
}
/**
* 发送数据
*/
public boolean sendData(int which, byte[] bytes) {
if (which < 0 || which >= mClientList.size()) {
return false;
}
Socket socket = mClientList.get(which);
if (socket != null && socket.isConnected()) {
try {
OutputStream out = socket.getOutputStream();
out.write(bytes);
out.flush();
return true;
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
public boolean sendData(int which, String data) {
return sendData(which, data.getBytes(Charset.forName("UTF-8")));
}
public void close() {
if (mServer != null) {
try {
mServer.close();
mServer = null;
} catch (IOException e) {
e.printStackTrace();
}
}
for (Socket client : mClientList) {
try {
client.close();
client = null;
} catch (IOException e) {
e.printStackTrace();
}
}
mClientList.clear();
}
public Socket getClient(int which) {
return which < 0 || which >= mClientList.size() ? null : mClientList.get(which);
}
public int getClientCount() {
return mClientList.size();
}
private void logI(String msg) {
Log.i(TAG, msg);
}
}
SimpleTCPServer demo:
public class ServerActivity extends AppCompatActivity {
private TextView tv_msg;
private Button btn_restart;
private SimpleTCPServer mSimpleTCPServer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_server);
initData();
initView();
}
private void initData() {
mSimpleTCPServer = new SimpleTCPServer(mHandler);
mSimpleTCPServer.listen(9000);
}
private void initView() {
tv_msg = (TextView) findViewById(R.id.tv_msg);
btn_restart = (Button) findViewById(R.id.btn_restart);
tv_msg.setMovementMethod(ScrollingMovementMethod.getInstance());
btn_restart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mSimpleTCPServer != null) {
for (int i = 0; i < mSimpleTCPServer.getClientCount(); i++) {
mSimpleTCPServer.sendData(i, "Push From Sever, 服务器发送数据来了".getBytes(Charset.forName("UTF-8")));
}
}
}
});
btn_restart.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
tv_msg.setText("");
return true;
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
mSimpleTCPServer.close();
}
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
WeakReference refActivity;
public MyHandler(ServerActivity activity) {
refActivity = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
ServerActivity activity = refActivity.get();
int which = msg.what;
byte[] data = (byte[]) msg.obj;
String result = new String(data, Charset.forName("UTF-8"));
activity.tv_msg.append("第" + which + "位访客"
+ activity.mSimpleTCPServer.getClient(which).getInetAddress().getHostAddress() + ":"
+ result + "\n");
activity.mSimpleTCPServer.sendData(msg.what, "Hi你好,I am server".getBytes(Charset.forName("UTF-8")));
}
}
}
SimpleTCPClient:
public abstract class SimpleTCPClient {
private static final String TAG = SimpleTCPClient.class.getSimpleName();
private static final int CONNECT_TIMEOUT = 5000;
private static final int INPUT_STREAM_READ_TIMEOUT = 300;
private Socket mSocket;
private InputStream mInputStream;
private OutputStream mOutputStream;
public SimpleTCPClient() {
}
/**
* 连接主机
* @param host 主机IP地址
* @param port 主机端口
*/
public void connect(final String host, final int port) {
if (mSocket != null) {
close();
}
new Thread(new Runnable() {
@Override
public void run() {
try {
mSocket = new Socket();
SocketAddress socketAddress = new InetSocketAddress(host, port);
// 设置连接超时时间
mSocket.connect(socketAddress, CONNECT_TIMEOUT);
if (mSocket.isConnected()) {
logI("connected to host success");
// 设置读流超时时间,必须在获取流之前设置
mSocket.setSoTimeout(INPUT_STREAM_READ_TIMEOUT);
mInputStream = mSocket.getInputStream();
mOutputStream = mSocket.getOutputStream();
new ReceiveThread().start();
} else {
mSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 接收进程
*/
class ReceiveThread extends Thread {
@Override
public void run() {
super.run();
while (mSocket != null && mSocket.isConnected() && mInputStream != null) {
// 读取流
byte[] data = new byte[0];
byte[] buf = new byte[1024];
int len;
try {
while ((len = mInputStream.read(buf)) != -1) {
byte[] temp = new byte[data.length + len];
System.arraycopy(data, 0, temp, 0, data.length);
System.arraycopy(buf, 0, temp, data.length, len);
data = temp;
}
} catch (IOException e) {
// e.printStackTrace();
}
// 处理流
if (data.length != 0) {
processData(data);
}
}
}
}
/**
* process data from received,which is not run in the main thread。
*/
public abstract void processData(byte[] data);
/**
* 发送数据
*/
public void send(byte[] data) {
if (mSocket != null && mSocket.isConnected() && mOutputStream != null) {
try {
mOutputStream.write(data);
mOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void send(String data) {
send(data.getBytes(Charset.forName("UTF-8")));
}
public void close() {
if (mSocket != null) {
try {
mInputStream.close();
mOutputStream.close();
mSocket.close();
mInputStream = null;
mOutputStream = null;
mSocket = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
public Socket getSocket() {
return mSocket;
}
private void logI(String msg) {
Log.i(TAG, msg);
}
}
SimpleTCPClient demo:
public class ClientActivity extends AppCompatActivity {
private Button btn_send;
private TextView tv_msg;
private SimpleTCPClient mSimpleTCPClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client);
initData();
initView();
}
private void initData() {
mSimpleTCPClient = new SimpleTCPClient() {
@Override
public void processData(byte[] data) {
sendMessageToActivity(new String(data, Charset.forName("UTF-8")));
}
};
mSimpleTCPClient.connect("192.168.1.214", 9000);
}
private void initView() {
tv_msg = (TextView) findViewById(R.id.tv_msg);
btn_send = (Button) findViewById(R.id.btn_send);
tv_msg.setMovementMethod(ScrollingMovementMethod.getInstance());
btn_send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mSimpleTCPClient.send("Hello,from client:测试数据来了");
}
});
btn_send.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
tv_msg.setText("");
return true;
}
});
}
private void sendMessageToActivity(String msg) {
Message message = Message.obtain();
message.obj = msg + "\n";
mHandler.sendMessage(message);
}
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference refActivity;
MyHandler(ClientActivity activity) {
refActivity = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
ClientActivity activity = refActivity.get();
activity.tv_msg.append((CharSequence) msg.obj);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mSimpleTCPClient.close();
}
}
本来想在9月1日前写完,目录早就计划好了,但发现越写越多,到现在也只写完TCP。考虑到代码太多,还是分二篇来写。也算作是休息会。先定一个小目标,比如挣它一个亿﹏﹏﹏﹏﹏﹏