打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Android开发:Socket简介与使用解析
userphoto

2016.12.08

关注

前言

  • Socket的使用在Android的网络编程中非常重要
  • 今天我将带大家全面了解Socket及其使用方法

目录


目录

1.网络基础

1.1 计算机网络分层

计算机网络分为五层:物理层、数据链路层、网络层、运输层、应用层


计算机网络

其中:

  • 网络层:负责根据IP找到目的地址的主机
  • 运输层:通过端口把数据传到目的主机的目的进程,来实现进程与进程之间的通信

1.2 端口号(PORT)

端口号规定为16位,即允许一个IP主机有2的16次方65535个不同的端口。其中:

  • 0~1023:分配给系统的端口号

    我们不可以乱用

  • 1024~49151:登记端口号,主要是让第三方应用使用

    但是必须在IANA(互联网数字分配机构)按照规定手续登记,

  • 49152~65535:短暂端口号,是留给客户进程选择暂时使用,一个进程使用完就可以供其他进程使用。

在Socket使用时,可以用1024~65535的端口号

1.3 C/S结构

  • 定义:即客户端/服务器结构,是软件系统体系结构
  • 作用:充分利用两端硬件环境的优势,将任务合理分配到Client端和Server端来实现,降低了系统的通讯开销。

    Socket正是使用这种结构建立连接的,一个套接字接客户端,一个套接字接服务器。

如图:


Socket架构

可以看出,Socket的使用可以基于TCP或者UDP协议。

1.4 TCP协议

  • 定义:Transmission Control Protocol,即传输控制协议,是一种传输层通信协议

    基于TCP的应用层协议有FTP、Telnet、SMTP、HTTP、POP3与DNS。

  • 特点:面向连接、面向字节流、全双工通信、可靠

    • 面向连接:指的是要使用TCP传输数据,必须先建立TCP连接,传输完成后释放连接,就像打电话一样必须先拨号建立一条连接,打完后挂机释放连接。

    • 全双工通信:即一旦建立了TCP连接,通信双方可以在任何时候都能发送数据。

    • 可靠的:指的是通过TCP连接传送的数据,无差错,不丢失,不重复,并且按序到达。

    • 面向字节流:流,指的是流入到进程或从进程流出的字符序列。简单来说,虽然有时候要传输的数据流太大,TCP报文长度有限制,不能一次传输完,要把它分为好几个数据块,但是由于可靠性保证,接收方可以按顺序接收数据块然后重新组成分块之前的数据流,所以TCP看起来就像直接互相传输字节流一样,面向字节流。

  • TCP建立连接
    必须进行三次握手:若A要与B进行连接,则必须
    • 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认。即A发送信息给B
    • 第二次握手:服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认。即B收到连接信息后向A返回确认信息
    • 第三次握手:客户端收到服务器的(SYN+ACK)报文段,并向服务器发送ACK报文段。即A收到确认信息后再次向B返回确认连接信息

      此时,A告诉自己上层连接建立;B收到连接信息后告诉上层连接建立。


TCP三次握手

这样就完成TCP三次握手 = 一条TCP连接建立完成 = 可以开始发送数据

  1. 三次握手期间任何一次未收到对面回复都要重发。
  2. 最后一个确认报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态。

为什么TCP建立连接需要三次握手?

答:防止服务器端因为接收了早已失效的连接请求报文从而一直等待客户端请求,从而浪费资源

  • “已失效的连接请求报文段”的产生在这样一种情况下:Client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。
  • 这是一个早已失效的报文段。但Server收到此失效的连接请求报文段后,就误认为是Client再次发出的一个新的连接请求。
  • 于是就向Client发出确认报文段,同意建立连接。
  • 假设不采用“三次握手”:只要Server发出确认,新的连接就建立了。
  • 由于现在Client并没有发出建立连接的请求,因此不会向Server发送数据。
  • 但Server却以为新的运输连接已经建立,并一直等待Client发来数据。>- 这样,Server的资源就白白浪费掉了。

采用“三次握手”的办法可以防止上述现象发生:

  • Client不会向Server的确认发出确认
  • Server由于收不到确认,就知道Client并没有要求建立连接
  • 所以Server不会等待Client发送数据,资源就没有被浪费
  • TCP释放连接
    TCP释放连接需要四次挥手过程,现在假设A主动释放连接:(数据传输结束后,通信的双方都可释放连接)

    • 第一次挥手:A发送释放信息到B;(发出去之后,A->B发送数据这条路径就断了)
    • 第二次挥手:B收到A的释放信息之后,回复确认释放的信息:我同意你的释放连接请求

    • 第三次挥手:B发送“请求释放连接“信息给A

    • 第四次挥手:A收到B发送的信息后向B发送确认释放信息:我同意你的释放连接请求

      B收到确认信息后就会正式关闭连接;
      A等待2MSL后依然没有收到回复,则证明B端已正常关闭,于是A关闭连接


TCp四次握手

为什么TCP释放连接需要四次挥手?

为了保证双方都能通知对方“需要释放连接”,即在释放连接后都无法接收或发送消息给对方

  • 需要明确的是:TCP是全双工模式,这意味着是双向都可以发送、接收的
  • 释放连接的定义是:双方都无法接收或发送消息给对方,是双向的
  • 当主机1发出“释放连接请求”(FIN报文段)时,只是表示主机1已经没有数据要发送 / 数据已经全部发送完毕;

    但是,这个时候主机1还是可以接受来自主机2的数据。

  • 当主机2返回“确认释放连接”信息(ACK报文段)时,表示它已经知道主机1没有数据发送了

    但此时主机2还是可以发送数据给主机1

  • 当主机2也发送了FIN报文段时,即告诉主机1我也没有数据要发送了

    此时,主机1和2已经无法进行通信:主机1无法发送数据给主机2,主机2也无法发送数据给主机1,此时,TCP的连接才算释放

1.5 UDP协议
  • 定义:User Datagram Protocol,即用户数据报协议,是一种传输层通信协议。

    基于UDP的应用层协议有TFTP、SNMP与DNS。

  • 特点:无连接的、不可靠的、面向报文、没有拥塞控制

    • 无连接的:和TCP要建立连接不同,UDP传输数据不需要建立连接,就像写信,在信封写上收信人名称、地址就可以交给邮局发送了,至于能不能送到,就要看邮局的送信能力和送信过程的困难程度了。

    • 不可靠的:因为UDP发出去的数据包发出去就不管了,不管它会不会到达,所以很可能会出现丢包现象,使传输的数据出错。

    • 面向报文:数据报文,就相当于一个数据包,应用层交给UDP多大的数据包,UDP就照样发送,不会像TCP那样拆分。

    • 没有拥塞控制:拥塞,是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象,就像交通堵塞一样。TCP建立连接后如果发送的数据因为信道质量的原因不能到达目的地,它会不断重发,有可能导致越来越塞,所以需要一个复杂的原理来控制拥塞。而UDP就没有这个烦恼,发出去就不管了。
  • 应用场景
    很多的实时应用(如IP电话、实时视频会议、某些多人同时在线游戏等)要求源主机以很定的速率发送数据,并且允许在网络发生拥塞时候丢失一些数据,但是要求不能有太大的延时,UDP就刚好适合这种要求。所以说,只有不适合的技术,没有真正没用的技术。

1.6 HTTP协议

详情请看我写的另外一篇文章你需要了解的HTTP知识都在这里了!


2. Socket的定义

  • 即套接字,是其中计算机网络中运输层和应用层之间的一种一个中间抽象层,也是一个编程接口
  • 成对出现,一对套接字Socket的组成就是 Socket ={(IP地址1:PORT端口号),(IP地址2:PORT端口号)}

3. Socket具体使用

  • Socket可基于TCP或者UDP协议,但TCP更加常用

    由于TCP比UDP常用,所以下面实例中的Socket将基于TCP协议


3.1 实例Demo1

下面是一个简单的、基于TCP协议的Socket连接demo

客户端:AndroidStudio实现
服务器端:Eclipse实现

服务器端(用eclipse编译):

package scut;import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class Sock { /** * 服务器端 * * @author Administrator * */ // 程序入口 public static void main(String args[]) { try { // 建立一个ServletSocket ,监听对应的端口,用于监听客户端的连接请求 ServerSocket serverSocket = new ServerSocket(40004); while (true) { // 循环不断接收客户端的请求 System.out.println('等待客户端请求....'); Socket socket = serverSocket.accept(); // 等待接收 System.out.println('收到请求,服务器建立连接...'); // 返回数据 OutputStream os = socket.getOutputStream(); String msg = '服务器已连接成功...'; os.write(msg.getBytes('utf-8')); os.close(); socket.close(); } } catch (Exception e) { e.printStackTrace(); } } }

输出

收到请求,服务器建立连接...等待客户端请求....收到请求,服务器建立连接...等待客户端请求....

客户端(用Android studio编译):

package scut.myserversocket;import android.os.Bundle;import android.support.design.widget.FloatingActionButton;import android.support.design.widget.Snackbar;import android.support.v7.app.AppCompatActivity;import android.support.v7.widget.Toolbar;import android.util.Log;import android.widget.TextView;import android.widget.Toast;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;public class MainActivity extends AppCompatActivity { private TextView tv; String str; boolean running = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.TV); findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { running = !running; new Thread(){ //建一个线程防止阻塞UI线程 public void run(){ super.run(); while (running){ try { Socket socket = new Socket('192.168.56.1',40004); //建立连接,因为genymotion的模拟器的本地ip不同于一般的模拟器,所以ip地址要用这个 sleep(1000); // 获取服务器返回的数据 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); System.out.println('服务器数据:' + (str = br.readLine())); os.close(); br.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); tv.setText(str); } }); }}

最后还要在Mainifest
输出:

服务器数据:服务器已连接成功...服务器数据:服务器已连接成功...服务器数据:服务器已连接成功...

3.2 实例Demo2

一个较复杂、基于TCP的Socket通信demo(可以双向互发信息)

  1. 服务器端和客户端都是在Android实现
  2. 需要两台Android手机连到同一个wifi,使它们处于同一个网段,才能用Socket访问IP地址实现客户端和服务器端的连接通信。

服务器端代码.

xml代码:

MainActivity.java:

package scut.serversocket;import android.os.Handler;import android.os.Message;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;import android.widget.Toast;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.io.UnsupportedEncodingException;import java.net.ServerSocket;import java.net.Socket;public class MainActivity extends AppCompatActivity { private TextView tv = null; private EditText et = null; private TextView IPtv = null; private Button btnSend = null; private Button btnAcept = null; private Socket socket; private ServerSocket mServerSocket = null; private boolean running = false; private AcceptThread mAcceptThread; private ReceiveThread mReceiveThread; private Handler mHandler = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.tv); et = (EditText) findViewById(R.id.etSend); IPtv = (TextView) findViewById(R.id.tvIP); btnAcept = (Button) findViewById(R.id.btnAccept); btnSend = (Button) findViewById(R.id.btnSend); mHandler = new MyHandler(); btnSend.setEnabled(false);//设置发送按键为不可见 btnAcept.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //开始监听线程,监听客户端连接 mAcceptThread = new AcceptThread(); running = true; mAcceptThread.start(); btnSend.setEnabled(true);//设置发送按键为可见 IPtv.setText('等待连接'); btnAcept.setEnabled(false); } }); //发送数据按钮 btnSend.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { OutputStream os = null; try { os = socket.getOutputStream();//获得socket的输出流 String msg = et.getText().toString()+'\n';// System.out.println(msg); os.write(msg.getBytes('utf-8'));//输出EditText的内容 et.setText('');//发送后输入框清0 os.flush(); } catch (IOException e) { e.printStackTrace(); }catch (NullPointerException e) { displayToast('未连接不能输出');//防止服务器端关闭导致客户端读到空指针而导致程序崩溃 } } }); } //定义监听客户端连接的线程 private class AcceptThread extends Thread{ @Override public void run() {// while (running) { try { mServerSocket = new ServerSocket(40012);//建立一个ServerSocket服务器端 socket = mServerSocket.accept();//阻塞直到有socket客户端连接// System.out.println('连接成功'); try { sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } Message msg = mHandler.obtainMessage(); msg.what = 0; msg.obj = socket.getInetAddress().getHostAddress();//获取客户端IP地址 mHandler.sendMessage(msg);//返回连接成功的信息 //开启mReceiveThread线程接收数据 mReceiveThread = new ReceiveThread(socket); mReceiveThread.start(); } catch (IOException e) { e.printStackTrace(); }// } } } //定义接收数据的线程 private class ReceiveThread extends Thread{ private InputStream is = null; private String read; //建立构造函数来获取socket对象的输入流 public ReceiveThread(Socket sk){ try { is = sk.getInputStream(); } catch (IOException e) { e.printStackTrace(); } } @Override public void run() { while (running) { try { sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(is, 'UTF-8')); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } try { //读服务器端发来的数据,阻塞直到收到结束符\n或\r read = br.readLine(); System.out.println(read); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException e) { running = false;//防止服务器端关闭导致客户端读到空指针而导致程序崩溃 Message msg2 = mHandler.obtainMessage(); msg2.what = 2; mHandler.sendMessage(msg2);//发送信息通知用户客户端已关闭 e.printStackTrace(); break; } //用Handler把读取到的信息发到主线程 Message msg = mHandler.obtainMessage(); msg.what = 1; msg.obj = read; mHandler.sendMessage(msg); } } } private void displayToast(String s) { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); } class MyHandler extends Handler{//在主线程处理Handler传回来的message @Override public void handleMessage(Message msg) { switch (msg.what){ case 1: String str = (String) msg.obj; tv.setText(str); break; case 0: IPtv.setText('客户端'+msg.obj+'已连接'); displayToast('连接成功'); break; case 2: displayToast('客户端已断开'); //清空TextView tv.setText(null);// IPtv.setText(null); try { socket.close(); mServerSocket.close(); } catch (IOException e) { e.printStackTrace(); } btnAcept.setEnabled(true); btnSend.setEnabled(false); break; } } } @Override protected void onDestroy() { mHandler.removeCallbacksAndMessages(null);//清空消息队列,防止Handler强引用导致内存泄漏 }}

客户端代码:
xml代码:

MainActivity.java:

package scut.clientsocket;import android.os.Handler;import android.os.Message;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.TextView;import android.widget.Toast;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.net.Socket;public class MainActivity extends AppCompatActivity implements View.OnClickListener { private TextView tv; private EditText et; private EditText IPet; private Handler myhandler; private Socket socket; private String str = ''; boolean running = false; private Button btnSend; private Button btnStart; private Button btnStop; private StartThread st; private ReceiveThread rt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.TV); et = (EditText) findViewById(R.id.et); IPet = (EditText) findViewById(R.id.IPet); btnSend = (Button) findViewById(R.id.btnSend); btnStart = (Button) findViewById(R.id.btnStart); btnStop = (Button) findViewById(R.id.btnStop); setButtonOnStartState(true);//设置按键状态为可开始连接 btnSend.setOnClickListener(this); btnStart.setOnClickListener(this); btnStop.setOnClickListener(this); myhandler = new MyHandler();//实例化Handler,用于进程间的通信 } @Override public void onClick(View v) { switch (v.getId()){ case R.id.btnStart: //按下开始连接按键即开始StartThread线程 st = new StartThread(); st.start(); setButtonOnStartState(false);//设置按键状态为不可开始连接 break; case R.id.btnSend: // 发送请求数据 OutputStream os = null; try { os = socket.getOutputStream();//得到socket的输出流 //输出EditText里面的数据,数据最后加上换行符才可以让服务器端的readline()停止阻塞 os.write((et.getText().toString()+'\n').getBytes('utf-8')); et.setText('');//发送后输入框清0// System.out.println(et.getText().toString()+'\n'); } catch (IOException e) { e.printStackTrace(); } break; case R.id.btnStop: running = false; setButtonOnStartState(true);//设置按键状态为不可开始连接 try { socket.close(); } catch (NullPointerException e) { e.printStackTrace(); displayToast('未连接成功'); } catch (IOException e) { e.printStackTrace(); } break; } }private class StartThread extends Thread{ @Override public void run() { try { socket = new Socket(IPet.getText().toString(),40012);//连接服务端的IP //启动接收数据的线程 rt = new ReceiveThread(socket); rt.start(); running = true; System.out.println(socket.isConnected()); if(socket.isConnected()){//成功连接获取socket对象则发送成功消息 Message msg0 = myhandler.obtainMessage(); msg0.what=0; myhandler.sendMessage(msg0); } } catch (IOException e) { e.printStackTrace(); } }} private class ReceiveThread extends Thread{ private InputStream is; //建立构造函数来获取socket对象的输入流 public ReceiveThread(Socket socket) throws IOException { is = socket.getInputStream(); } @Override public void run() { while (running) { InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); try { //读服务器端发来的数据,阻塞直到收到结束符\n或\r System.out.println(str = br.readLine()); } catch (NullPointerException e) { running = false;//防止服务器端关闭导致客户端读到空指针而导致程序崩溃 Message msg2 = myhandler.obtainMessage(); msg2.what = 2; myhandler.sendMessage(msg2);//发送信息通知用户客户端已关闭 e.printStackTrace(); break; } catch (IOException e) { e.printStackTrace(); } //用Handler把读取到的信息发到主线程 Message msg = myhandler.obtainMessage(); msg.what = 1;// } msg.obj = str; myhandler.sendMessage(msg); try { sleep(400); } catch (InterruptedException e) { e.printStackTrace(); } } Message msg2 = myhandler.obtainMessage(); msg2.what = 2; myhandler.sendMessage(msg2);//发送信息通知用户客户端已关闭 } } private void displayToast(String s)//Toast方法 { Toast.makeText(this, s, Toast.LENGTH_SHORT).show(); } private void setButtonOnStartState(boolean flag){//设置按钮的状态 btnSend.setEnabled(!flag); btnStop.setEnabled(!flag); btnStart.setEnabled(flag); IPet.setEnabled(flag); }class MyHandler extends Handler{//在主线程处理Handler传回来的message @Override public void handleMessage(Message msg) { switch (msg.what) { case 1: String str = (String) msg.obj; System.out.println(msg.obj); tv.setText(str);//把读到的内容更新到UI break; case 0: displayToast('连接成功'); break; case 2: displayToast('服务器端已断开'); tv.setText(null); setButtonOnStartState(true);//设置按键状态为可开始 break; } }}}

PS:在操作Socket的输入输出流的时候一定不能close()关闭,一关闭的话就会导致整个Socket关闭,这里搞了我好久。

效果图:
操作:首先服务端点建立按钮,建立ServerSocket,然后客户端输入服务端的IP地址,点开始连接,提示连接成功之后可以发送消息,点取消连接之后回复到初始状态
服务端界面(未连接):


客户端界面(未连接):


服务端界面(连接):


客户端界面(连接):


服务端界面(接收到消息):


客户端界面(接收到消息):




4. 总结

  • 相信大家已经非常了解关于Socket的使用
  • 接下来,我会继续介绍Android中其他相关知识,有兴趣可以继续关注Carson_Ho的安卓开发笔记

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
「面试」计算机网络基础知识点
Android:这是一份很详细的 Socket 使用攻略
Wireshark抓取Android数据包
java 基于TCP协议的Socket编程和通信
JAVA实现TCP通信
初学者第70节网络编程-Socket(一)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服