Javaは、ネットワークプログラミングをマルチスレッド化 - 研究JavaソケットおよびLinuxソケット

  今日のインターネット時代には、ネットワークは、ネットワーク通信が不可分である、それはQQ、マイクロ文字、またはオンラインゲームであるかどうか、非常に重要であり、そして時にJavaのWeb開発の最もホットな言語として、私たちはJavaのネットワークプログラミングとの接触を持っていると信じて、ネットワークというのjava通信はそれを呼ばれるシステムレベルのインターフェイス?今日、私はJavaのソケットとLinuxのソケット間の接続の種類を探求するために一緒に皆をもたらすだろう。

  それは、ネットワークトラフィックは一言で言えば、コンピュータネットワーク、それについて話すことはできませんどのようになると、二つの主要なネットワークコミュニティネットワーク階層モデルがあります:OSIは7層を有する、OSIおよびTCP / IP、TCP / IPネットワークは4層に分割されます今、TCP / IPネットワークでは、我々は、事実上の標準モデル、両者を合わせた画像に以下に記載する一般的なTCP / IPプロトコル層モデル5を、言われています:

  

 

  実際のソケットで、それの層は、あなたがネットワークへのソケットインタフェースを介して、その後、ソケットは、アプリケーション層とトランスポート層との間に配置され、その層に言わなければならない場合は、ネットワーク通信インタフェースシステムは、をご提供している中で、そのソケット・インタフェース抽象的、シールド下にある層は、印象を与えることが直接大幅プログラマーの作業を簡素化され、相互に通信し、同じソケットのソケットを使用しているようで、非常に多くの複雑なプロトコルです、プログラマが物事の下部に心配する必要はありません、唯一のアプリケーション層とに対処するためのトランスポート層とのソケットインタフェースを介して、当然のことながら、実際には、時間のプログラマのほとんどは、アプリケーション層のみを心配する必要があります。一般的には、2つのトランスポート層プロトコルが存在する、すなわち、接続指向TCPおよびUDPコネクションレスプロトコル、いわゆる接続指向の輸送手段が発注され、エラーのない、それはより多くの時間がかかりますが、有用である可能性がある。何も接続ではありませんベストエフォートを意味し、エラーアウトポイントは重要ではありません。

  ソケットトランスポート層は、サービスを利用します、そしてもちろんソケットインタフェースはまた、TCPソケットとポイントベースのUDPのソケットに基づいて、UDPは、今日のTCPソケットに基づいて、比較的単純であるが、例えば、ソケットを書くためにJava言語を使用すると、ネットワークチャットをマルチスレッドJavaソケットの後ろの原理を探求するためのプログラム。

 

  コードを書く前に、まず簡単な紹介のJavaネットワークプログラミングソケット・インタフェースが最も重要です:  

  1、ScoketもIPアドレスとポート番号で構成され、「ソケット」として知られている、あなたがネットワーク上で一意にプロセスを識別することを言うことができ、アプリケーションは、通常「ソケットの要求や応答ネットワークを介してネットワークへの要求を行います; Javaのソケットライブラリと位置付けjava.netパッケージ内のServerSocket。サーバ側ServerSocketの接続が成功しながら、ソケットは、ネットワーク接続を確立するために使用され、アプリケーションがソケット・インスタンスの両端を持って、本実施例の動作は、セッションを完了するために必要。ネットワーク接続の場合、ソケットは等しくなく、違いはない、なぜなら、サーバ側またはクライアントないと自分の仕事をソケット・クラスおよびそのサブクラスを介して行われているかどうかソケットServerSocketを、異なるレベルを生成します完全に

  図2に示すように、リンク確立ソケットは、3つのステップに分け:
         1.監視サーバ
         2、クライアント要求
         のリンク3.
         4.コミュニケーション
  。3、ソケット特徴:
          1。TCPベースのリンク、データ伝送が保証される
          長い確立するのに適した2ランダムにオフになり、HTTPとは異なり、リンク、
          3.Socketプログラムは、インスタントメッセージングで使用されます

  人気話す、ソケットは、電話番号がありますが、他の当事者が、我々は最初のソケットである電話番号、電話の向こう側を知らなければならない場合があることを確認するために電話をかけたい場合は、各携帯電話は、とき放送、電話番号を持っています番号電話が両者の間の接続を確立することで、他の側を押し答えキーギターを、鳴ったとき、双方がお互いの話をすることができるようになります。

  入力された後に、Javaのマルチスレッドのネットワークチャットプログラムを書き始め、最初のサーバー・コードは次のとおりです。

パッケージソケット。

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!然后断开与连接。

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

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

 

 

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

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

  

 

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

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 <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       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(),调用链如下:

 

 

 

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

2. bind()

#include <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   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 <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   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 <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   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 <sys/types.h>          /* See NOTES */
   #include <sys/socket.h>
   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 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 调用过程,这就对程序员显得更加友好了,但是作为一个计算机专业的学生,我们不能只使用“黑盒子”,而不去打开“黑盒子”去看看其内部构造,只有挖掘到事物内部、知其然,知其所以然,我们才能创造属于我们自己的“黑盒子”!

 

 

 


  

 

 

 

おすすめ

転載: www.cnblogs.com/mjc-blog/p/12006430.html