1.Socket 定义

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

传输层实现端到端的通信,因此,每一个传输层连接由两个端点/。那么,传输层连接的断电是什么呢,不是主机,不是主机的IP地址,不是应用进程,也不是传输层的协议端口,传输层连接的端点叫做套接字(socket)。根据RFC793的定义,端口号拼接到IP地址就构成了套接字。所谓套接字,实际上是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16为的主机端口号,即形如(主机IP地址:端口号)。例如,如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23).总之,套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层链接唯一地被通信两端的两个端点(即两个套接字)所确定。

2. Hello/Hi

下面用Java简单的实现一个基于Socket通信的hello/hi程序:

Server端:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
class Server {
    private Socket server;
    private Server() {
        try {
            System.out.println("启动服务器!");
            ServerSocket serverSocket = new ServerSocket(8888);
            server = serverSocket.accept();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void listen() {
        try {
            System.out.println("Listening!......");
            //从Socket中获得输入流
            InputStreamReader in = new InputStreamReader(server.getInputStream());
            BufferedReader br = new BufferedReader(in);
            //读取输入流中的一行并输出
            System.out.println(br.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void send(String msg) {
        try {
            PrintWriter out = new PrintWriter(server.getOutputStream(), true);
            out.println("Server:" + msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Server se = new Server();
        String msg = "";
        Scanner cin = new Scanner(System.in);
        while (!msg.equals("#")) {
            se.listen();
            System.out.print("输入信息:");
            msg = cin.nextLine();
            se.send(msg);
        }
    }
}

Client端:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
class Client {
    private Socket client;
    private Client() {
        try {
            client = new Socket("127.0.0.1", 8888);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void send(String msg) {
        try {
            PrintWriter out = new PrintWriter(client.getOutputStream(), true);
            out.println("Client:" + msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void listen() {
        try {
            System.out.println("Listening!......");
            InputStreamReader in = new InputStreamReader(client.getInputStream());
            BufferedReader br = new BufferedReader(in);
            System.out.println(br.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        String msg = "";
        Client c = new Client();
        Scanner cin = new Scanner(System.in);
        while (!msg.equals("#")) {
            System.out.print("Input: ");
            msg = cin.nextLine();
            c.send(msg);
            c.listen();
        }
    }
}

执行结果:

Client发送hello,Server回应hi 

           

 调用栈分析

为什么java实现socket通信这么方便呢,这就需要我们深入源码去一探究竟了,这里以Server端为例,追踪调用栈:

实例化ServerSocket时,构造函数会调用ServerSocket的bind()方法,

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
        ...if (port >= 0 && port <= 65535) {
            if (backlog < 1) {
                backlog = 50;
            }
            try {
                this.bind(new InetSocketAddress(bindAddr, port), backlog);
            } catch (SecurityException var5) {
                this.close();
                throw var5;
            } catch (IOException var6) {
                this.close();
                throw var6;
            }
        } else {
            throw new IllegalArgumentException("Port value out of range: " + port);
        }
    }
public void bind(SocketAddress endpoint, int backlog) throws IOException {
       ...try {
                        SecurityManager security = System.getSecurityManager();
                        if (security != null) {
                            security.checkListen(epoint.getPort());
                        }

                        this.getImpl().bind(epoint.getAddress(), epoint.getPort());
                        this.getImpl().listen(backlog);
                        this.bound = true;
                    } catch (SecurityException var5) {
                        this.bound = false;
                        throw var5;
                    } catch (IOException var6) {
                        this.bound = false;
                        throw var6;
                    }
    ...
    }

该方法会调用继承自抽象类AbstractPlainSocketImpl的PlainSocketImpl的socketBind()方法,在该方法中会调用native方法bind0(),从而实现将一个socket连接绑定到指定的本地IP地址和端口号。

注:native关键字标注的方法为本地方法,一般是用其他语言写成的函数,常用来实现java语言对OS底层接口的访问。Java语言本身不能直接对操作系统底层进行操作,但是java允许程序通过Java本机接口JNI,使用C/C++等其他语言实现这种操作。在Windows系统中,使用native关键字标注的本地方法在编译时会生成一个动态链接库(.dll文件)为Java语言提供响应的本地服务。

void socketBind(InetAddress address, int port) throws IOException {
        int nativefd = this.checkAndReturnNativeFD();
        if (address == null) {
            throw new NullPointerException("inet address argument is null.");
        } else if (preferIPv4Stack && !(address instanceof Inet4Address)) {
            throw new SocketException("Protocol family not supported");
        } else {
            bind0(nativefd, address, port, useExclusiveBind);
            if (port == 0) {
                this.localport = localPort0(nativefd);
            } else {
                this.localport = port;
            }

            this.address = address;
        }
    }

接着,同样的步骤从ServerSocket的listen()方法可以一直追溯到PlainSocketImpl的sokectListen()方法的listen0(),该方法主要为了设置允许的最大连接请求队列长度,当请求队列满时,拒绝后来的连接请求。

最后,同样,从ServerSocket类的accept()追溯到accept0(),等待连接请求的到来。

具体调用关系如下图所示:(图转自https://www.cnblogs.com/Mr-Tiger/p/11969934.html

 

3. Java Socekt API与Linux Socket API对比

Linux提供的响应Socket API在sys/socket.h中,分别为:

int socket(int domain, int type, int protocol);

从函数名就可以看出,socket函数可以创建一个socket,

其中,domain参数告诉系统使用哪个底层协议族,对TCP/IP协议族而言,该参数应该设置为PF_INET或PF_INET6,没错,分别对应IPv4和IPv6,对于UNIX本地域协议族而言,该参数应该设置为PF_UNIX,具体socket系统支持的所有协议族,请读者自行参考其man手册。

type参数指定服务类型,主要有SOCK_STREAM流服务和SOCK_UGRAM数据报服务,对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。

protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常是唯一的,几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。

熟悉UNIX/Linux的同学应该知道,在这类系统中,所有的东西都是文件,socket也不例外,可读,可写,可控制,可关闭的文件描述符。socket函数调用成功时返回一个socket文件描述符

int bind(int sockfd, const struct sockaddr *addr, socklen_t addelen)

bind将my_addr所指的socket地址分配给未命名的socketfd文件描述符,addrlen参数指出该socket地址的长度。bind成功时返回0,失败则返回-1并设置errno,常见为EACCES和EASSRINUSE,前者代表被绑定的地址是受保护的地址,仅超级用户能够访问,后者表示被绑定的地址正在使用中。

值得注意的是,Client端通常不需要bind socket而是采用匿名方式,OS自动分配socket地址。

int listen(int sockfd, int backlog);

socket被bind之后还不能马上接收客户的连接,需要创建一个监听队列存放待处理的客户连接,服务端通过listen进行监听。

sockfd参数指定被监听的socket,backlog参数体时内核监听队列的最大长度,如果超过,服务器将不再受理新的客户端连接,客户端也将收到ECONNREFUSED错误信息。listen成功返回0,失败返回-1并设置errno。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

最后一步为accept,其中sockfd参数是执行过listen系统调用的监听socket,addr参数用来获取被接受连接的远程socket地址,该socket地址的长度由addlen参数指出,accpet成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信,accept失败时返回-1并设置errno。

 

其实,Java也是调用Linux网络API实现网络通信的,通过调用这些系统API来实现它的底层功能的,从调用分析时贴出的源码中可以看出,在Java的ServerSocket创建时就对方法进行了socket的bind和listen操作,一个方法就封装了3个API,即ServerSocket的实例化过程就对应了Linux中的socket(),bind(),listen(),而Java中的accept对应了Linux的accept函数,相关对应关系如下图所示(图来源https://blog.csdn.net/vipshop_fin_dev/article/details/102966081):

 所以,Java将这一切全都封装起来,这使得面向网络的编程对于Java程序员来说变得十分简单,我们只需要知道使用的哪一个类(实际上也就是ServerSocket和Socket两个类),为它们传入必要的地址参数,就能够轻松实现Socket通信。

posted on 2019-12-10 13:08  SeanXXX  阅读(351)  评论(0编辑  收藏  举报