java多线程网络编程——探究java socket与linux socket

  在当今互联网时代,网络显得尤为重要,不论是QQ、微信,还是网络游戏,都离不开网络通信,而java作为当web开发最火的语言,相信大家都接触过java网络编程,那java网络通信中调用了系统级的哪些接口呢?今天,我就带着大家共同探究java socket与linux socket之间的千丝万缕。

  说到网络通信怎么能不谈计算机网络呢,简而言之,网络界主要有两种网络分层模型:即OSI和TCP/IP,OSI有7层,TCP/IP则将网络分为4层,现在TCP/IP模型是事实上的网络标准,而我们结合两者,一般都说TCP/IP 5层协议模型,下面给一张图来说明:

  java多线程网络编程——探究java socket与linux socket_第1张图片

 

  那socket接口在哪一层呢,事实上socket是系统为我们提供的网络通信接口,如果非要说它在哪一层的话,那么socket就位于应用层和传输层之间,通过socket接口对网络的抽象,屏蔽了下面各层那么多复杂的协议,给人感觉好像是用socket套接字直接与对方通信一样,这样大大简化了程序员的工作,使得程序员根本不需要关心底层的东西,只需要通过socket接口与应用层和传输层打交道即可,当然其实大部分时间程序员只需要关心应用层即可。一般来讲,传输层有两大协议,即面向连接的TCP和无连接的UDP协议,所谓面向连接是指传输是有序的、无差错的,可能更费时,但很有用;而无连接是指尽最大努力交付,出点差错也无所谓。

  socket会用到运输层的服务,那么当然socket接口也有基于tcp的socket和基于udp的socket之分,udp比较简单,今天就以基于tcp的socket为例,使用java语言编写一个socket多线程网络聊天程序,探究java socket背后的工作原理。

 

  在编写代码之前先简单介绍下java网络编程中最重要的socket接口:  

  1、Scoket又称“套接字”,其由IP地址和端口号组成,可以说它唯一标识了网络上的某个进程,应用程序通常通过“套接字”向网络发出请求或者应答网络请求;在 java中Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的,在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同的级别,不管是Socket还是ServerSocket他们的工作都是通过Socket类和其子类来完成的

  2、建立Socket链接可分三个步骤:
         1.服务器监听
         2.客户端发出请求
         3.建立链接
         4.通信
  3、Socket特点:
          1.基于TCP链接,数据传输有保障
          2.适用于建立长时间的链接,不像HTTP那样会随机关闭
          3.Socket编程应用于即时通讯

  通俗点讲,socket是一个电话号码,每个手机都有一个电话号码,当你想打电话给对方时,首先要知道对方的电话号码即socket,对方的手机要保证有话费的,当播出号码的时候,对方手机响了,按下吉接听键,这是就建立了双方的连接,就能双方相互通话了。

  下面进入正题,开始写java多线程网络聊天程序,首先是服务器代码:

package socket;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/***
 * 多线程TCP服务器,为每个连接创立一个线程
 * @author mjc
 * @version 1.1 2019-12-4
 */

public class TCPServer {
    public static void main(String[] args){
        try(ServerSocket s = new ServerSocket(8189))
        {
            int i = 1;
            while (true){
                Socket incoming = s.accept();
                System.out.println("连接序号:"+i);
                Runnable r = new ServerThread(incoming);
                Thread t = new Thread(r);
                t.start();
                i++;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class ServerThread implements Runnable{
    private Socket incoming;
    public ServerThread(Socket incoming){
        this.incoming = incoming;
    }
    public void run(){
        try(InputStream inputStream = incoming.getInputStream();
            OutputStream outputStream = incoming.getOutputStream()){
            Scanner in = new Scanner(inputStream,"GBK");
            PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true);
            //out.println("Hello! Enter BYE to exit.");
            boolean done = false;
            while (!done&&in.hasNextLine()){
                String line = in.nextLine();
                System.out.println("客户端发来: "+line);
                //out.println("Echo: "+line);
                if(line.trim().equals("BYE")) {
                    System.out.println("我发给客户端: BYE,BYE!");
                    System.out.println("与客户端连接断开");
                    out.println("BYE,BYE!");
                    done = true;}
                else
                    System.out.println("我发给客户端: hi!");
                    out.println("hi!");
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  服务器主要设计了两个类,一个是TCPServer,一个是实现了Runnable接口的线程类ServerThread,用来实现多线程,在主方法中,首先用ServerSocket s = new ServerSocket(8189)
创建一个端口为8189的监听端口,然后循环使用Socket incoming = s.accept();接受客户端的连接并建立相应的socket,每建立一个连接便启动一个服务器线程,这样便可以和多个客户端进行通信。
服务器主要是收到客户端的字符串,并回送一个hi,直到客户端发出BYE,服务器便向对方回送BYE,BYE!,然后断开与客户端的连接。
  接下来编写两个客户端程序,将同时与服务器进行通信,客户端1源码:
package socket;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

public class TCPClient1 {
    public static void main(String[] args){
        try (Socket s = new Socket("127.0.0.1",8189);
             InputStream inputStream = s.getInputStream();
             OutputStream outputStream = s.getOutputStream())
        {
            System.out.println("客户端1连接到服务器成功!");
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            Scanner in = new Scanner(inputStream,"GBK");
            PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true);
            System.out.println("开始与服务器聊天,说BYE去结束聊天.");
            boolean done =false;
            while(!done){
                String line = br.readLine();
                if(line.equals("BYE"))
                    done = true;
                out.println(line);
                System.out.println("服务器说: "+in.nextLine());
            }
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  客户端2源码:

package socket;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;

public class TCPClient2 {
    public static void main(String[] args){
        try (Socket s = new Socket("127.0.0.1",8189);
             InputStream inputStream = s.getInputStream();
             OutputStream outputStream = s.getOutputStream())
        {
            System.out.println("客户端2连接到服务器成功!");
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            Scanner in = new Scanner(inputStream,"GBK");
            PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true);
            System.out.println("开始与服务器聊天,说BYE去结束聊天.");
            boolean done =false;
            while(!done){
                String line = br.readLine();
          System.out.println("客户端发来: "+line);
if(line.equals("BYE"))
            System.out.println("我发给客户端: BYE,BYE!"); done
= true; out.println(line); System.out.println("服务器说: "+in.nextLine()); } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }

  客户端首先使用 Socket s = new Socket("127.0.0.1",8189);建立了一个对方ip为127.0.0.1(即本地主机),端口为8189的socket,客户端会向这个socket发出建立请求,如果建立成功则返回一个socket s,用户可在命令行敲出字符串,这个消息会发送到指定地址的服务器进程,当客户端输入BYE的时候,服务器会回送一个BYE,BYE!然后断开与连接。

  现在让它们跑起来试试,先开启服务端,然后开启两个客户端:

java多线程网络编程——探究java socket与linux socket_第2张图片

java多线程网络编程——探究java socket与linux socket_第3张图片

java多线程网络编程——探究java socket与linux socket_第4张图片

  现在在客户端输入BYE试试:

java多线程网络编程——探究java socket与linux socket_第5张图片

java多线程网络编程——探究java socket与linux socket_第6张图片

java多线程网络编程——探究java socket与linux socket_第7张图片

 

 

  可以看到,用java来写网络通信程序还是比较简单的,服务端只用到了 ServerSocket类及其accept()方法和socket类,客户端也就用到了socket类,这样两者便能通畅的对话了,java语言为我们提供的网络编程API让我们不必关心底层的细节,然而其实它的通信也是利用了系统的socket API,在探究java的socket之前我们先来看看linux 为我们提供的socket API:

  这里再提一次,socket就是抽象封装了传输层以下软硬件行为,为上层应用程序提供进程/线程间通信管道。就是让应用开发人员不用管信息传输的过程,直接用socket API就OK了。贴个TCP的socket示意图体会一下:

  java多线程网络编程——探究java socket与linux socket_第8张图片

 

   现在以TCP client/server模型为例子看一下linux socket通信的整个过程:

java多线程网络编程——探究java socket与linux socket_第9张图片

socket API函数如下:

socket: establish socket interface
gethostname: obtain hostname of system
gethostbyname: returns a structure of type hostent for the given host name
bind: bind a name to a socket
listen: listen for connections on a socket
accept: accept a connection on a socket
connect: initiate a connection on a socket
setsockopt: set a particular socket option for the specified socket.
close: close a file descriptor
shutdown: shut down part of a full-duplex connection

1. socket()

#include           /* See NOTES */
       #include 
       int socket(int domain, int type, int protocol);
    
    - 参数说明
    domain: 设定socket双方通信协议域,是本地/internet ip4 or ip6
       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)

    type: 设定socket的类型,常用的有
        SOCK_STREAM - 一般对应TCP、sctp
        SOCK_DGRAM - 一般对应UDP
        SOCK_RAW - 
        
    protocol: 设定通信使用的传输层协议
    常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,可以设置为0,系统自己选定。注意protocol和type不是随意组合的。

 

  socket() API是在glibc中实现的,该函数又调用到了kernel的sys_socket(),调用链如下:

java多线程网络编程——探究java socket与linux socket_第10张图片

 

 

 

  详细的kernel实现我没有去读,大体上这样理解。调用socket()会在内核空间中分配内存然后保存相关的配置。同时会把这块kernel的内存与文件系统关联,以后便可以通过filehandle来访问修改这块配置或者read/write socket。操作socket就像操作file一样,应了那句unix一切皆file。提示系统的最大filehandle数是有限制的,/proc/sys/fs/file-max设置了最大可用filehandle数。

2. bind()

#include           /* See NOTES */
   #include 
   int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
   
   参数说明
   sockfd:之前socket()获得的file handle
   addr:绑定地址,可能为本机IP地址或本地文件路径
   addrlen:地址长度
   
   功能说明
   bind()设置socket通信的地址,如果为INADDR_ANY则表示server会监听本机上所有的interface,如果为127.0.0.1则表示监听本地的process通信(外面的process也接不进啊)。

3. listen()

  #include           /* See NOTES */
   #include 
   int listen(int sockfd, int backlog);
   
   参数说明
   sockfd:之前socket()获得的file handle
   backlog:设置server可以同时接收的最大链接数,server端会有个处理connection的queue,listen设置这个queue的长度。
   
   功能说明
   listen()只用于server端,设置接收queue的长度。如果queue满了,server端可以丢弃新到的connection或者回复客户端ECONNREFUSED。

4. accept()

 #include           /* See NOTES */
   #include 
   int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
   
   参数说明:
   addr:对端地址
   addrlen:地址长度
   
   功能说明:
   accept()从queue中拿出第一个pending的connection,新建一个socket并返回。
   新建的socket我们叫connected socket,区别于前面的listening socket。
   connected socket用来server跟client的后续数据交互,listening socket继续waiting for new connection。
   当queue里没有connection时,如果socket通过fcntl()设置为 O_NONBLOCK,accept()不会block,否则一般会block。

5. connect()

 #include           /* See NOTES */
   #include 
   int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
   
   参数说明:
   sockfd: socket的标示filehandle
   addr:server端地址
   addrlen:地址长度
   
   功能说明:
   connect()用于双方连接的建立。
   对于TCP连接,connect()实际发起了TCP三次握手,connect成功返回后TCP连接就建立了。  
   对于UDP,由于UDP是无连接的,connect()可以用来指定要通信的对端地址,后续发数据send()就不需要填地址了。
   当然UDP也可以不使用connect(),socket()建立后,在sendto()中指定对端地址。

   以上就是系统为我们提供的主要socket接口函数以及C/S模型使用TCP通信的过程,这些函数都是用C语言实现的,java底层也是用C语言写的,

  现在让我们来追踪java网络程序中调用的socket接口过程:

  上面的客户端只是实例化Socket类便可向对方建立连接,就先从Socket谈起吧,在Idea IDE中追踪Socket:

(1)起始、

Socket s = new Socket("127.0.0.1",8189)

(2)追踪Socket、

 public Socket(String host, int port) throws UnknownHostException, IOException {
        this(host != null ? new InetSocketAddress(host, port) : new InetSocketAddress(InetAddress.getByName((String)null), port), (SocketAddress)null, true);
    }

(3)发现在调用构造方法中,又调用了构造函数,跟踪这个this()构造函数:

private Socket(SocketAddress address, SocketAddress localAddr, boolean stream) throws IOException {
        this.created = false;
        this.bound = false;
        this.connected = false;
        this.closed = false;
        this.closeLock = new Object();
        this.shutIn = false;
        this.shutOut = false;
        this.oldImpl = false;
        this.setImpl();
        if (address == null) {
            throw new NullPointerException();
        } else {
            try {
                this.createImpl(stream);
                if (localAddr != null) {
                    this.bind(localAddr);
                }

                this.connect(address);
            } catch (IllegalArgumentException | SecurityException | IOException var7) {
                try {
                    this.close();
                } catch (IOException var6) {
                    var7.addSuppressed(var6);
                }

                throw var7;
            }
        }
    }

  ok,终于找到你了,这个构造函数中产生了一个流,先不管这个,可以认为是一个通信的管道,重点是这里调用了 this.bind(localAddr)和 this.connect(address)方法,是不是很熟悉,没错跟linux socket接口函数一样,一个用来绑定地址并监听,一个用来向服务端请求连接。

  现在再来跟踪下服务端的ServerSocket类和accept()方法:

(1)起始、

ServerSocket s = new ServerSocket(8189)

(2)跟踪ServerSocket、

public ServerSocket(int port) throws IOException {
        this(port, 50, (InetAddress)null);
    }

(3)跟踪这个this构造方法:

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
        this.created = false;
        this.bound = false;
        this.closed = false;
        this.closeLock = new Object();
        this.oldImpl = false;
        this.setImpl();
        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);
        }
    }

  可以看到,先判断端口号是否合理,然后调用了 this.bind()方法绑定地址并开始监听这个端口;

(4)跟踪bind()方法:

 

public void bind(SocketAddress endpoint, int backlog) throws IOException {
        if (this.isClosed()) {
            throw new SocketException("Socket is closed");
        } else if (!this.oldImpl && this.isBound()) {
            throw new SocketException("Already bound");
        } else {
            if (endpoint == null) {
                endpoint = new InetSocketAddress(0);
            }

            if (!(endpoint instanceof InetSocketAddress)) {
                throw new IllegalArgumentException("Unsupported address type");
            } else {
                InetSocketAddress epoint = (InetSocketAddress)endpoint;
                if (epoint.isUnresolved()) {
                    throw new SocketException("Unresolved address");
                } else {
                    if (backlog < 1) {
                        backlog = 50;
                    }

                    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;
                    }
                }
            }
        }
    }

 

  发现了什么,bind()函数里又调用了listen()方法;简直和linux socket通信过程一模一样啊。

this.getImpl().listen(backlog);

 

  接下来看看 Socket incoming = s.accept()又做了什么:

(1)起始:

Socket incoming = s.accept();

(2)跟踪accept():

public Socket accept() throws IOException {
        if (this.isClosed()) {
            throw new SocketException("Socket is closed");
        } else if (!this.isBound()) {
            throw new SocketException("Socket is not bound yet");
        } else {
            Socket s = new Socket((SocketImpl)null);
            this.implAccept(s);
            return s;
        }
    }

  可以看到accept接受连接并返回一个socket对象,服务端便可利用这个socket对象与客户端通信。

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

  总结:本次带着大家使用java提供的socket编写了多线程的网络聊天程序,通过对java socket接口调用的一步步跟踪,我们发现虽然使用java socket编程非常简单,但是其内部也是调用了一系列的如同linux socket通信的socket函数,废话不多说,用图来直观的感受一下:java多线程网络编程——探究java socket与linux socket_第11张图片

 

 

   上图是服务端的java socket调用过程,即当我们在java创建一个tcp连接时,需要首先实例化java的ServerSocket类,其中封装了底层的socket()方法、bind()方法、listen()方法。

  客户端java通过使用实例化Socket对象向服务端请求建立连接,在实例化Socket对象时,同样调用了与linux socket API一样的socket()、connect()方法,即可以说是java客户端中的Socket封装了linux socket中的socket()、connect()方法,通过java socket的这种封装屏蔽了底层一些我们看不到的socket 调用过程,这就对程序员显得更加友好了,但是作为一个计算机专业的学生,我们不能只使用“黑盒子”,而不去打开“黑盒子”去看看其内部构造,只有挖掘到事物内部、知其然,知其所以然,我们才能创造属于我们自己的“黑盒子”!(码了一天了,求支持一波)

 

 

 


  

 

 

 

你可能感兴趣的:(java多线程网络编程——探究java socket与linux socket)