Android 网络编程(一) 网络基本知识的了解

引言

  Android网络编程知识是Android开发过程中必不可少的内容,在网络开发的过程中,我们通常会用到像Volley、OkHttp、Retrofit这些高度封装好的框架,这使得我们的开发很便利但也屏蔽了相关的技术细节。而作为想要进一步的开发者来说,我们不但要会用,有时候更要理解其实现的原理,理解了后更能促进我们更好的使用这些框架。Ok,这篇文章我们讲一讲网络的基本知识。

OSI 七层网络模型

  为了使不同的厂家生产的计算机能够通信,以便能在更大的范围内建立计算机网络,国际标准化组织(ISO)在 1978 年提出了“开放系统互联参考模型”,即 OSI/RM 模型(Open System Interconnection/Reference Model)。他将计算机网络体系结构的通信协议划分为了七层,自下而上依次分别是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。其中低四层完成数据传输,高三层面向用户。

物理层

物理层(Physical Layer)在局部局域网上传送数据帧(data frame),它负责管理计算机通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机适配器等。

数据链路层

数据链路层(Data Link Layer)负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时,会形成帧。数据链表头(DLH)是包含了物理地址和错误侦测及改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串。例如以太网、无线局域网(Wi-Fi)和通用分组无线服务(GPRS)等。
分为两个子层:逻辑链路控制(logic link control,LLC)子层和介质访问控制(media access control,MAC)子层。

网络层

网络层(Network Layer) 决定数据的路径选择和转寄,将网络表头(NH)加至数据包,以形成分组。网络表头包含了网络数据。例如:互联网协议(IP)等。

传输层

传输层(Transport Layer)把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。例如:传输控制协议(TCP)等。

会话层

会话层(Session Layer)负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接。

表达层

表达层(Presentation Layer)把数据转换为能与接收者的系统格式兼容并适合传输的格式。

应用层

应用层(Application Layer)提供为应用软件而设的接口,以设置与另一应用软件之间的通信。例如: HTTP,HTTPS,FTP,TELNET,SSH,SMTP,POP3等。

TCP/IP 四层模型

  由于 OSI/RM 模型过于复杂难以实现,现实中广泛使用的是 TCP/IP 模型。TCP/IP 是一个协议集,是由 ARPA ( Advanced Research Projects Agency Network 高等研究计划署网络 ) 于 1977 到 1979 年推出的一种网络体系结构和协议规范。随着 Internet 的发展,TCP/IP 得到进一步的研究和推广,成为 Internet 上的 “通用模型”。
  TCP/IP 模型在 OSI 模型的基础上进行了简化,去掉了OSI参考模型中的会话层和表示层(这两层的功能被合并到应用层实现),同时将OSI参考模型中的数据链路层和物理层合并为主机到网络层。变成了四层,从下到上分别为:网络接口层、网络层、传输层、应用层。与 OSI 体系结构对比如下:

OSI七层模型

TCP/IP 四层模型

网络协议

应用层

(Application)

应用层

HTTP(超文本传输协议)

HTTPS(超文本传输安全协议)

FTP(文件传输协议)

SMTP(简单邮件传输协议)

DNS(域名服务)

等等

表示层

(Presentation)

会话层

(Session)

传输层

(Transport)

传输层

TCP(传输控制协议)

UDP(用户数据报协议)

网络层

(Network)

网际互连层

IP(网际协议)

ICMP(网络控制消息协议)

IGMP(网络组管理协议)

数据链路层

(Data Link)

网络接口层

以太网

Wi-Fi

等等

物理层

(Physical)

IP 协议

  网际协议(英语:Internet Protocol,缩写为IP),又译互联网协议,是用于分组交换数据网络的一种协议。

  IP是在TCP/IP协议族中网络层的主要协议,任务仅仅是根据源主机和目的主机的地址来传送数据。为此目的,IP定义了寻址方法和数据报的封装结构。第一个架构的主要版本,现在称为IPv4,仍然是最主要的互联网协议,尽管世界各地正在积极部署IPv6。IP协议的作用在于把各种数据包准确无误的传递给对方,其中两个重要的条件是IP地址和MAC地址(Media Access Control Address)。由于IP地址是稀有资源,不可能每个人都拥有一个IP地址,所以我们通常的IP地址是路由器给我们生成的IP地址,路由器里面会记录我们的MAC地址。而MAC地址是全球唯一的,除去人为因素外不可能重复。举一个现实生活中的例子,IP地址就如同是我们居住小区的地址,而MAC地址就是我们住的那栋楼那个房间那个人。

TCP 协议

  传输控制协议(英语:Transmission Control Protocol,缩写为TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

  TCP 协议被认为是稳定的协议,因为它有以下特点:

  • 面向连接,“三次握手”
  • 双向通信
  • 保证数据按序发送,按序到达
  • 超时重传

  要使用 TCP 传输数据,必须先建立连接,传输完成后释放连接。分别对应常说的“三次握手”、“四次挥手”。

TCP 的三次握手与四次挥手

  三次握手与四次分手的流程如下所示:
在这里插入图片描述

三次握手

  • 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
  • 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RCVD状态;
  • 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将ACK设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

  当客户端和服务器通过三次握手建立了TCP连接后,当数据传输完毕,断开连接时就需要进行TCP的四次挥手。

四次挥手

  TCP 协议中,在通信结束后,需要断开连接,这需要通过四次挥手,客户端或服务器均可主动发起,主动的一方先断开,这里以客户端先断开为例:

  • 第一次分手:客户端设置Seq和ACK,向服务器端发送一个FIN报文段;此时,客户端进入FIN_WAIT_1状态;客户端表示没有数据要发送给服务器端了;
  • 第二次分手:服务器端收到了客户端发送的FIN报文段,向主机1回一个ACK报文段,ACK为Sequence Number加1;客户端进入FIN_WAIT_2状态;服务器告诉客户端,我“同意”你的关闭请求;
  • 第三次分手:服务器向客户端发送FIN报文段,请求关闭连接,同时服务器进入LAST_ACK状态;服务器告诉客户端,我也没啥数据要发给你了,请求断开。
  • 第四次分手:客户端收到服务器发送的FIN报文段,向服务器发送ACK报文段,然后客户端进入TIME_WAIT状态;服务器收到客户端的ACK报文段以后,就关闭连接;此时,客户端等待2MSL后依然没有收到回复,则证明服务端已正常关闭,那好,客户端也可以关闭连接了。

为什么要进行三次握手

  三次握手的目的是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。

为什么要进行四次挥手

  TCP 连接是全双工的,每一端都可以同时发送和接受数据,关闭的时候两端都要关闭各自两个方向的通道,总共相当于要关闭四个。

以客户端发起关闭为例
1.服务器读通道关闭
​ 2.客户端写通道关闭
​ 3.客户端读通道关闭
​ 4.服务器写通道关闭

客户端为什么要等待 2MSL?

  MSL(Maximum Segment Life),是 TCP 对 TCP Segment 生存时间的限制
  客户端在发出确认服务端关闭的 ACK 后,它没有办法知道对方是否收到这个消息,于是需要等待一段时间,如果服务端没有收到关闭的消息后会重新发出 FIN 报文,这样客户端就知道自己上条消息丢了,需要再发一次;如果等待的这段时间没有在收到 FIN 的重发报文,说明它的确已经收到断开的消息并且已经断开了。
  这个等待时间至少是:客户端的 timeout + FIN 的传输时间,为了保证可靠,采用更加保守的等待时间 2MSL。

UDP协议

  用户数据报协议(英语:User Datagram Protocol,缩写为UDP),又称用户数据报文协议,是一个简单的面向数据报的传输层协议。

  UDP是一个不可靠的或者说无连接的协议,他有以下的特点:

  • UDP 缺乏可靠性。UDP 本身不提供确认,序列号,超时重传等机制。UDP 数据报可能在网络中被复制,被重新排序。即 UDP 不保证数据报会到达其最终目的地,也不保证各个数据报的先后顺序,也不保证每个数据报只到达一次
  • UDP 数据报是有长度的。每个 UDP 数据报都有长度,如果一个数据报正确地到达目的地,那么该数据报的长度将随数据一起传递给接收方。而 TCP 是一个字节流协议,没有任何(协议上的)记录边界。
  • UDP 是无连接的。UDP 客户和服务器之前不必存在长期的关系。UDP 发送数据报之前也不需要经过握手创建连接的过程。
  • UDP 支持多播和广播。

  UDP 协议没有 TCP 协议稳定,因为它不建立连接,也不按顺序发送,可能会出现丢包现象,使传输的数据出错。但是有得就有失,UDP 的效率更高,因为 UDP 头包含很少的字节,比 TCP 负载消耗少,同时也可以实现双向通信,不管消息送达的准确率,只负责无脑发送。UDP 服务于很多知名应用层协议,比如 NFS(网络文件系统)、SNMP(简单网络管理协议)。UDP 一般多用于 IP 电话、网络视频等容错率强的场景。

Socket (套接字)

在这里插入图片描述
  Socket 作为应用层和传输层之间的桥梁,与之关系最大的两个协议就是传输层中的 TCP 和 UDP协议。
  Socket 是对 TCP/IP 协议族的一种封装,是应用层与TCP/IP协议族通信的中间软件抽象层。TCP或者UDP的报文中,除了数据本身还包含了包的信息,比如目的地址和端口,包的源地址和端口,以及其他附加校验信息。从设计模式的角度看来,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
  Socket 还可以认为是一种网络间不同计算机上的进程通信的一种方法,利用三元组(ip地址,协议,端口)就可以唯一标识网络中的进程,网络中的进程通信可以利用这个标志与其它进程进行交互。
  Socket 起源于 Unix ,Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开(open) –> 读写(write/read) –> 关闭(close)”模式来进行操作。因此 Socket 也被处理为一种特殊的文件。
在这里插入图片描述
  如图所示,Socket 被称为“套接字”,它把复杂的 TCP/IP 协议簇隐藏在背后,为用户提供简单的客户端到服务端接口,让我们感觉这边输入数据,那边就直接收到了数据,像一个“管道”一样。

Socket 的使用

Socket 的基本操作有以下几部分:

  1. 连接远程机器
  2. 发送数据
  3. 接收数据
  4. 关闭连接
  5. 绑定端口
  6. 监听到达数据
  7. 在绑定的端口上接受来自远程机器的连接

要实现客户端与服务端的通信,双方都需要实例化一个 Socket。
在 Java 中,客户端可以实现上面的 1、2、3、4、,服务端实现 5、6、7.
Java.net 中为我们提供了使用 TCPUDP 通信的两种 Socket:

  • ServerSocket:流套接字,TCP
  • DatagramSocket:数据报套接字,UDP

使用 TCP 通信的 Socket 流程

服务端:

  1. 调用 ServerSocket(int port) 创建一个 ServerSocket,绑定到指定端口
  2. 调用 accept() 监听连接请求,如果客户端请求连接则接受,返回通信套接字
  3. 调用 Socket 类的 getOutputStream()getInputStream() 获取输出和输入流,进行网络数据的收发
  4. 关闭套接字

客户端:

  1. 调用 Socket() 创建一个流套接字,连接到服务端
  2. 调用 Socket 类的 getOutputStream()getInputStream() 获取输出和输入流,进行网络数据的收发
  3. 关闭套接字

使用 UDP 通信的 Socket 流程

服务端:

  1. 调用 DatagramSocket(int port) 创建一个数据报套接字,绑定到指定端口
  2. 调用 DatagramPacket(byte[] buf, int length) 建立一个字节数组,以接受 UDP 包
  3. 调用 DatagramSocketreceive() 接收 UDP 包
  4. 调用 DatagramSocket.send() 发送 UDP 包
  5. 关闭数据报套接字

客户端:

  1. 调用 DatagramSocket() 创建一个数据报套接字
  2. 调用 DatagramPacket(byte buf[], int offset, int length,InetAddress address, int port) 建立要发送的 UDP 包
  3. 调用 DatagramSocketreceive() 接收 UDP 包
  4. 调用 DatagramSocket.send() 发送 UDP 包
  5. 关闭数据报套接字

Demo演示:TCP 通信的 Socket 实现跨进程聊天

配置

  首先我们来实现服务端,当然要使用Socket我们需要在AndroidManifest.xml声明如下的权限:

   <uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

  我们需要实现一个远程的Service来当作聊天程序的服务端,AndroidManifest.xml文件中配置service:

<service
            android:name=".SocketServerService"
            android:process=":remote" />

实现Service

  接下来我们在Service启动时,在线程中建立TCP服务,我们监听的是8688端口,等待客户端连接,当客户端连接时就会生成Socket。通过每次创建的Socket就可以和不同的客户端通信了。当客户端断开连接时,服务端也会关闭Socket并结束结束通话线程。服务端首先会向客户端发送一条消息:“您好,我是服务端”,并接收客户端发来的消息,将收到的消息进行加工再返回给客户端。

public class SocketServerService extends Service {

    private boolean isServiceDestroy = false;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        new Thread(new TcpServer()).start();
        Log.d("SocketServerServer", "服务端已onCreate: ");
    }

    private class TcpServer implements Runnable {

        @Override
        public void run() {
            ServerSocket serverSocket;
            try {
                //监听指定的端口
                serverSocket = new ServerSocket(SocketClientActivity.CODE_SOCKET_CONNECT);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
            while (!isServiceDestroy) {
                try {
                    // 如果客户端请求连接则接受,返回通信套接字
                    final Socket client = serverSocket.accept();
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                // 响应给客户端信息
                                responseClient(client);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void responseClient(Socket client) throws IOException {
        // 获取输入流 用于接收客户端消息
        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
        // 获取输出流 用于向客户端发送消息
        PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())),true);
        out.println("服务端已连接");
        while (!isServiceDestroy) {
            String str = in.readLine();
            Log.d("SocketServerServer", "服务器收到的信息: " + str);
            if (TextUtils.isEmpty(str)) {
                Log.d("SocketServerServer", "服务器收到的信息为空,已断开连接 ");
                break;
            }
            // 从客户端收到的消息加工再发送给客户端
            out.println("收到了客户端发来的消息------" + str);
        }
        in.close();
        out.close();
        client.close();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        isServiceDestroy = true;
    }
}

实现聊天程序客户端

  客户端activity_socket_client.xml 的布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_message"
        android:layout_width="match_parent"
        android:layout_height="400dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/et_receive"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="2" />

        <Button
            android:id="@+id/bt_send"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:text="向服务器发消息" />
    </LinearLayout>
</RelativeLayout>

  客户端Activity会在onCreate方法中启动服务端,并开启线程连接服务端Socket。为了确保能连接成功,采用了超时重连的策略,每次连接失败时都会重新建立连接。连接成功后,客户端会收到服务端发送的消息:“您好,我是服务端”,我们也可以在EditText输入字符并发送到服务端。

public class SocketClientActivity extends AppCompatActivity {

    public static final int CODE_SOCKET_CONNECT = 8688;
    private Button bt_send;
    private EditText et_receive;
    private PrintWriter mPrintWriter;
    private TextView tv_message;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_socket_client);
        initView();
        // 开启服务端
        Intent intent = new Intent(this, SocketServerService.class);
        startService(intent);
        new Thread() {
            @Override
            public void run() {
                // 连接到服务端
                connectSocketServer();
            }
        }.start();
    }

    private void initView() {
        et_receive = (EditText) findViewById(R.id.et_receive);
        bt_send = (Button) findViewById(R.id.bt_send);
        tv_message = (TextView) this.findViewById(R.id.tv_message);
        bt_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String msg = et_receive.getText().toString();
                //向服务器发送信息
                if (!TextUtils.isEmpty(msg) && null != mPrintWriter) {
                    mPrintWriter.println(msg);
                    tv_message.setText(tv_message.getText() + "\n" + getCurrentTime() + "\n" + "客户端:" + msg);
                    et_receive.setText("");
                }
            }
        });
    }

    private void connectSocketServer() {
        Socket socket = null;
        while (socket == null) {
            try {
                // 一个流套接字,连接到服务端
                socket = new Socket("localhost", CODE_SOCKET_CONNECT);
                // 获取到客户端的文本输出流
                mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),true);
            } catch (IOException e) {
                e.printStackTrace();
                SystemClock.sleep(1000);
            }
        }

        try {
            // 接收服务端发送的消息
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (!isFinishing()) {
                final String str = reader.readLine();
                if (str != null) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            tv_message.setText(tv_message.getText() + "\n" + getCurrentTime() + "\n" + "服务端:" + str);
                        }
                    });
                }
            }
            mPrintWriter.close();
            reader.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String getCurrentTime() {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
        return df.format(new Date());// new Date()为获取当前系统时间
    }
    
}

  最后我们来看下效果图:
在这里插入图片描述

总结

   点击可查看Socket 聊天程序的代码,以上就是网络基本知识内容的了解,下一篇文章打算写应用层的HTTP协议。

  • 8
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值