详解Java Socket的工作机制

4,159 阅读6分钟

Socket的来龙去脉

下面的分析主要是参阅了计算机网络(谢希仁第7版)进行总结的,从系统调用--->应用编程接口API--->套接字接口来分析Socket的来龙去脉,当然维基百科上也有对Socket的详细解释

1. 系统调用

大多数操作系统使用系统调用的机制在应用程序和操作系统之间传递控制权。对程序员来说,系统调用和一般程序设计中的函数调用非常相似

系统调用

2. 应用编程接口API

当某个应用进程启动系统调用时,控制权就从应用进程传递给了系统调用接口,此接口再将控制权传递给计算机的操作系统。操作系统将此调用转给某个内部过程,并执行所请求的操作。内部过程一旦执行完毕,控制权就又通过系统调用接口返回给应用进程。

系统调用接口实际上就是应用进程的控制权和操作系统的控制权进行转换的一个接口,即应用编程接口API

3. 套接字

由于TCP/IP协议族被设计成能够运行在多种操作系统的环境下,TCP/IP标准允许系统设计者能够选择有关API的具体实现细节。

目前,可供应用程序使用TCP/IP的应用编程接口API的最著名的是套接字接口

而套接字不是物理实体,而是一种抽象,套接字是提供应用程序创建和使用的数据结构

套接字

4. 套接字描述符

当应用进程(客户或者服务器)需要使用网络进行通信时,必须首先发出socket系统调用,请求操作系统为其创建一个"套接字"。这个调用的实际效果是请求操作系统把网络通信所需要的一些 系统资源(存储器空间,CPU时间,网络带宽等) 分配给该应用进程。

操作系统为这些资源的总和用一个叫做套接字描述符(socket descriptor)的号码(小的整数) 来表示,然后把这个套接字描述符返回给应用进程。

socket 描述符

Socket通信图示

socket

由图可以看出Socket通信与TCP/IP协议是分不开的,要使主机A和主机B能够通信,必须建立Socket连接,建立Socket连接必须通过底层TCP/IP协议来建立TCP连接。

Socket通信协议分析

上面就提到,Socket通信与TCP/IP协议是紧密相关的,关于Socket编程通信我们有两种协议可以选择,那就是常见的TCP协议和UDP协议

UDP协议

UDP协议是一种无连接的协议,也称为数据报协议。每次发送数据报时,需要同时发送本机的socket描述符(就是上面所说的套接字描述符)和接收端的socket描述符。所以,每次通信都要发送额外的数据。

TCP协议

TCP协议是一种有连接的协议,使用应用程序之前,必须先建立TCP连接。所以每次在进行通信之前那,我们需要先建立Socket连接,一个socket作为服务端监听请求,一个socket作为客户端进行连接请求。只有双方建立连接好以后,双方才可以通信。

两种协议区别及选择

简单分析两者的区别:

  • 在UDP中,每次发送数据报,需要附上本机的socket描述符和接收端的socket描述符.而TCP是基于连接的协议,在通信的socket之间需要在通信之前建立连接,即TCP的三次握手,,因此建立连接会有一定耗时
  • 在UDP中,数据报数据在大小有64KB的限制。而TCP不存在这样的限制,一旦TCP通信的socket对建立连接,他们通信类似IO流。
  • UDP是不可靠的协议,发送的数据报不一定会按照其发送顺序被接收端的socket接收。而TCP是一种可靠的协议。接收端收到的包的顺序和包在发送端的顺序大体一致(这里不讨论丢包的情况)

说到这,至于选择哪种协议,还是取决于你的使用场景,当然目前见得比较多就是基于TCP协议的Socket通信。当然一些实时性较高的一些服务,局域网的一些服务用UDP的多一些。

基于TCP协议的Java Socket编程实例

socket2

socket编程总的来说分为3步:建立连接,数据传送,连接释放。当然服务端程序和客户端程序具体步骤有些区别,如上图所示。

这里我们用java.net包下的ServerSocket类(主要用于服务端)和Socket类(用于建立连接)来实现一个Socket通信实例

服务端编写

package com.pjmike.Socket;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.SQLOutput;

/**
 * 服务端
 *
 * @author pjmike
 * @create 2018-08-12 17:43
 */
public class Server {
    private ServerSocket serverSocket = null;
    private Socket socket = null;
    private DataInputStream input = null;

    public Server(int port) {
        try {
            //绑定端口
            System.out.println("bind port ...");
            serverSocket = new ServerSocket(port);
            System.out.println("Server started and waiting a client ..");
            //调用accept()方法,提取连接请求
            socket = serverSocket.accept();
            //一般都是以字节传输
            input = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
            String line = "";
            while (!line.equals("exit")) {
                try {
                    //readUTF()方法需要读取writeUTF()写过来的数据
                    line = input.readUTF();
                    System.out.println("recd: " + line);
                } catch (IOException o) {
                    o.printStackTrace();
                }
            }
            //关闭连接
            System.out.println("connection closed ...");
            input.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Server server = new Server(5000);
    }
}

客户端程序

package com.pjmike.Socket;

import java.io.*;
import java.net.Socket;
import java.nio.Buffer;

/**
 * 客户端
 *
 * @author pjmike
 * @create 2018-08-12 17:52
 */
public class Client {
    private Socket socket = null;
    private DataOutputStream output = null;
    private BufferedReader input = null;

    public Client(String address, int port) {
        try {
            //建立连接
            socket = new Socket(address, port);
            System.out.println("Connected ...");
            //从控制台输入信息
            input = new BufferedReader(new InputStreamReader(System.in));
            output = new DataOutputStream(socket.getOutputStream());

        } catch (IOException e) {
            e.printStackTrace();
        }
        String line = "";
        while (!line.equals("exit")) {
            try {
                line = input.readLine();
                System.out.println("客户端输入的是: "+line);
                output.writeUTF(line);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            input.close();
            socket.close();
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Client client = new Client("localhost", 5000);
    }
}

测试

客户端

Connected ...
hello world
客户端输入的是: hello world
nihao
客户端输入的是: nihao
exit
客户端输入的是: exit

服务端

bind port ...
Server started and waiting a client ..
recd: hello world
recd: nihao
recd: exit
connection closed ...

当然有时我们也需要多个客户端连接到同一个服务端,这也不是什么难事,采用多线程的方式,让服务器循环调用accept()方法,每收到一个客户端请求就开启一个线程来进行处理。

小结

实际上,Socket就是一种进程间的通信机制,在Java Socket编程中,也是分三步走:建立通信链路,数据传输,链路关闭。而网络编程也是和Java I/O操作紧密结合在一起的,熟悉I/O操作也是必不可少的。

参考资料