zoukankan      html  css  js  c++  java
  • 简单hello/hi程序、分析及Java Socket API与Linux Socket API对比

    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通信。

  • 相关阅读:
    Nginx、PCRE和中文URL(UTF8编码)rewrite路径重写匹配问题
    Nginx 使用中文URL,中文目录路径
    再谈Nginx Rewrite, 中文URL和其它
    事务管理
    commons-dbcp连接池的使用
    JDBC操作简单实用了IOUtils
    JDBC进行处理大文件和批处理
    mysql日期函数(转)
    mysql约束(自己原先总结的有点不准)
    mysql笔记(前面自己写的不标准有些地方)
  • 原文地址:https://www.cnblogs.com/seanloveslife/p/12016017.html
Copyright © 2011-2022 走看看