• Java Socket编程基础(1)


    参考资料:

      《Java网络编程精解》 孙卫琴

    一、socket通信简介

      什么是socket,简单来说,在linux系统上,进程与进程之间的通信称为IPC,在同一台计算机中,进程与进程之间通信可以通过信号、共享内存的方式等等。

      不同计算机上的进程要进行通信的话就需要进行网络通信,而 socket通信就是不同计算机进程间通信中常见的一种方式,当然,同一台计算机也可以通过socket进行通信,比如mysql支持通过unix socket本地连接。

      

      socket在网络系统中拥有以下作用:

        (1) socket屏蔽了不同网络协议之间的差异

        (2) socket是网络编程的入口,它提供了大量的系统调用system call供程序员使用

        (3) linux的重要思想-一切皆文件,socket也是一种特殊的文件,网络通信在linux系统上同样是对文件的读 写操作

      linux上支持多种套接字种类,不同的套接字种类称为"地址簇",这是因为不同的套接字拥有不同的寻址方法。

      linux将其抽象为统一的BSD套接字接口,从而屏蔽了它们的区别,程序员关心了只是BSD套接字接口而已。

      

        以INET套接字为例:

        

      Linux在利用socket()进行系统调用时,需要传递套接字的地址族标识符、套接字类型以及协议、源代码:

      

    asmlinkage long sys_socket(int family, int type, int protocol)
    {
        int retval;
        struct socket *sock;
        retval = sock_create(family, type, protocol, &sock);
        if (retval < 0)
            goto out;
        retval = sock_map_fd(sock);
        if (retval < 0)
            goto out_release;
    out:
        /* It may be already another descriptor 8) Not kernel problem. */
        return retval;
    out_release:
        sock_release(sock);
        return retval;
    }

    不过对于用户而言,socket就是一种特殊的文件而已....

    二、TCP/IP以及SOCKET通信简介

    linux上网络通信实现由通信子网和资源子网2部分,

      通信子网位于linux内核空间,由linux内核实现,例如netfilter, tcp/ip协议栈等等功能

      资源子网由位于用户空间的程序实现,例如httpd, nginx, haproxy等等。

    计算机通信本质上是进程间的通信,一个计算机上可能运行着多个进程,我们使用端口来标记一个唯一的进程.

      0~1023:管理员才有权限使用,永久地分配给某应用使用;

      注册端口:1024~41951:只有一部分被注册,分配原则上非特别严格;

      动态端口或私有端口:41952+:

      

    tcp实现了以下功能:   

    ①连接建立

    ②将数据打包成段   MTU通常为1500以下

           校验和

    ③确认、重传以及超时机制

    ④排序

    序列号 32位  并非从0开始  过大的话循环轮换 从0开始

    ⑤流量控制  速度不同步2台数据的服务器    防止阻塞

    缓冲区  发送缓冲    接收缓冲

    滑动窗口 

    ⑥拥塞控制  多个进程通信

    慢启动   通过慢启动的方式探测,启动的时候很小  随后以指数级增长。

    拥塞避免算法

    tcp是一个有限状态机,三次连接,四次握手:

    注意:如果server端没有调用close()方法,可能出现大量连接处于CLOSE_WAIT状态,占用系统资源。

    三、Socket用法

      在C/S通信模式中,客户端主动创建与服务器连接的Socket,服务器收到了客户端的连接请求,也会创建与客户端连接的Socket。

      Socket是通信连接两端的收发器。服务器端监听在某个固定的端口上,每当有一个客户端连入时,都要创建一个socket文件,因此,linux系统打开文件数量直接影响着服务器端socket通信的并发能力。

    3.1 构造器

    当客户端创建Socket连接Server时,会随机分配端口,因此不用指定

        public static void main(String[] args) throws Exception{
            Socket socket = new Socket();
            //远程服务器地址
            SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
            //设定超时时长,单位ms,为0表示永不超时,超时则跑出SocketTimeoutException
            socket.connect(remoteAddr,60*1000);
        }

    设定客户端地址:

      在一个Socket对象中,同时包含了远程服务器的ip地址,端口信息,也要包含客户端的ip地址和端口信息,才能进行双向通信。

      默认,客户端不设置ip的话,客户端地址就是当前客户端主机的地址。构造器中支持显式指定。

    Socket的创建和连接中出现的各种异常说明:

    (1) UnkownHostException

      无法识别主机名或者ip地址,找不到server主机

    (2) ConnectException

      2种情况:

      没有服务器进程监听该端口

      服务器进程拒绝连接:比如服务器端设置了请求队列长度等情形。

    (3) SocketTimeoutException

      连接超时

    (4) BindException

      无法把Socket对象和指定的本地IP地址或者端口绑定,就会抛出这种异常

      例如:socket.bind(new InetSocketAddress.getByName("222.34.5.7"),1234);

      有可能本地主机没有改地址,或者该端口不能被使用,就会抛出该异常。

    3.2 获取Socket信息

     Socket包含了连接的相关信息,client和server的地址端口等等,还可以获取InputStream和OutputStream,以下是一个demo

    public class HTTPClient {
        String host="www.javathinker.org";
        int port=80;
        Socket socket;
    
        public void createSocket()throws Exception{
            socket=new Socket("www.javathinker.org",80);
        }
    
    
        public void communicate()throws Exception{
            StringBuffer sb=new StringBuffer("GET "+"/index.jsp"+" HTTP/1.1
    ");
            sb.append("Host: www.javathinker.org
    ");
            sb.append("Accept: */*
    ");
            sb.append("Accept-Language: zh-cn
    ");
            sb.append("Accept-Encoding: gzip, deflate
    ");
            sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)
    ");
            sb.append("Connection: Keep-Alive
    
    ");
    
            //发出HTTP请求
            OutputStream socketOut=socket.getOutputStream();
            socketOut.write(sb.toString().getBytes());
            socket.shutdownOutput();  //关闭输出流
    
            //接收响应结果
            InputStream socketIn=socket.getInputStream();
            ByteArrayOutputStream buffer=new ByteArrayOutputStream();
            byte[] buff=new byte[1024];
            int len=-1;
            while((len=socketIn.read(buff))!=-1){
                buffer.write(buff,0,len);
            }
    
            System.out.println(new String(buffer.toByteArray()));  //把字节数组转换为字符串
    
    
    /*
        InputStream socketIn=socket.getInputStream();
        BufferedReader br=new BufferedReader(new InputStreamReader(socketIn));
        String data;
        while((data=br.readLine())!=null){
          System.out.println(data);
        }
    */
            socket.close();
        }
    
        public static void main(String args[])throws Exception{
            HTTPClient client=new HTTPClient();
            client.createSocket();
            client.communicate();
        }
    }

    说明:上面方法用ByteArrayOutputStream来接收响应信息,也就是说响应会全部放置在内存中,在响应报文很长的时候这样很不明智,上面注释的代码中演示了如何使用BufferReader逐行进行读取。

    3.3 关闭Socket

    网络通信占用资源且有太多的因素,在finally代码块中关闭socket是省事的

    Socket类提供了3个状态测试方法:

    isClosed(): 如果Socket已经连接到远程主机,并且还没有关闭,则返回true

    isConnected(): 如果Socket曾经连接到过远程主机,返回true

    isBound(): 如果Socket和本地端口绑定,返回true

    因此确定一个Socket对象正在处于连接状态,可以用以下方式

    boolean isConnected = socket.isConnected() && !socket.isClosed();

    3.4 半关闭Socket

    socket通信也就是2个进程之间的通信,无论这2个进程是否处于同一个物理机器上,只需要向内核申请注册了端口就可以用ip+port进行唯一的标识。

    假设2个进程A和B之间通信,A如何通知B所有数据已经传输完毕呢?

    以上文中HttpClient为例

    StringBuffer sb=new StringBuffer("GET "+"/index.jsp"+" HTTP/1.1
    ");
            sb.append("Host: www.javathinker.org
    ");
            sb.append("Accept: */*
    ");
            sb.append("Accept-Language: zh-cn
    ");
            sb.append("Accept-Encoding: gzip, deflate
    ");
            sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)
    ");
            sb.append("Connection: Keep-Alive
    
    ");

    这实际上是典型的HTTP处理的方式,没有请求实体,因此以 表示结束,这就是一种约定方式。

    (1) 如果是字符流,可以以特殊字符作为结束标志,可以是 ,甚至于可以定义为"bye"

    (2) A可以先发送一个消息,事先声明了内容长度

    (3) A发送完毕之后,主动关闭Socket,B读取完了所有数据也关闭

    (4) shutdownInput, shutdownOutput 之关闭输出流或者输出流,但是这并不会释放资源,必须调用Socket的close()方法,才会释放资源

    3.5 Socket常用选项

      TCP_NODELAY: 表示立即发送数据,默认是false,表示开启Negale算法,true表示关闭缓冲,确保数据及时发送

        为false时,适合发送方需要发送大批量数据,并且接收方及时响应,这种算法通过减少传输数据的次数来提高效率

        为true,发送方持续的发送小批量数据,并且接受方不一定会立即响应数据

      SO_REUSEADDR: 表示是否允许重用Socket绑定的本地地址

      SO_TIMEOUT: 表示接收数据的等待超时时间

      SO_LINGER: 表示执行Socket的close()方法时,是否立即关闭底层的Socket,哪怕还有数据没有发送完也直接关闭

      SO_SNFBUF: 发送方缓冲区大小

      SO_RCVBUF: 接收数据的缓冲区大小

      SO_KEEPALIVE: 对于长时间处于空闲状态的Socket是否要自动关闭

    四、ServerSocket用法

    在C/S架构中,服务器端需要创建监听特定端口的ServerSocket,ServerSocket负责接收客户的连接请求。

    4.1 ServerSocket

    1.必须绑定一个端口

    ServerSocket serverSocket = new ServerSocket(80);

      如果无法绑定到一个端口,会抛出BindException,一般由以下原因:

       (1) 端口已经被占用

       (2) 某些操作系统中,只有超级用户才允许使用1-1023的端口

      如果port设置为0,表示操作系统来分配一个任意可用的端口,匿名端口,在某些场合,匿名端口有特殊作用

    2. 设定客户连接请求队列的长度

    一般的C/S架构中,服务器监听在某个固定的端口上,每来一个客户端连接,服务器都会创建一个socket文件维护与client的通信

    管理client连接的任务往往由操作系统来完成。操作系统把这些连接请求存储在一个先进先出的队列中。

    许多操作系统限定了队列的最大长度,一般是50。当client connections>50 时,服务器会拒绝新的请求。

    对于客户端而言,如果他的请求被server加入了队列,意味着连接成功,这个队列通常称为backlog.

    ServerSocket构造方法的backlog参数用来显示指定连接请求队列的长度,它将覆盖操作系统限定的最大长度,不过在以下情形,依旧采用操作系统的默认值:

    (1) backlog <= 0

    (2) without setting backlog

    (3) backlog参数的值 > 操作系统的允许范围

    演示: Server端设置backlog为3,不处理请求,client连接超过3会拒绝

    import java.io.*;
    import java.net.*;
    public class Server {
        private int port=8000;
        private ServerSocket serverSocket;
    
        public Server() throws IOException {
            serverSocket = new ServerSocket(port,3);  //连接请求队列的长度为3
            System.out.println("服务器启动");
        }
    
        public void service() {
            while (true) {
                Socket socket=null;
                try {
                    socket = serverSocket.accept();  //从连接请求队列中取出一个连接
                    System.out.println("New connection accepted " +
                            socket.getInetAddress() + ":" +socket.getPort());
                }catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    try{
                        if(socket!=null)socket.close();
                    }catch (IOException e) {e.printStackTrace();}
                }
            }
        }
    
        public static void main(String args[])throws Exception {
            Server server=new Server();
            Thread.sleep(60000*10);  //睡眠十分钟
            //server.service();
        }
    }
    import java.net.*;
    public class Client {
        public static void main(String args[])throws Exception{
            final int length=100;
            String host="localhost";
            int port=8000;
    
            Socket[] sockets=new Socket[length];
            for(int i=0;i<length;i++){  //试图建立100次连接
                sockets[i]=new Socket(host, port);
                System.out.println("第"+(i+1)+"次连接成功");
            }
            Thread.sleep(3000);
            for(int i=0;i<length;i++){
                sockets[i].close();  //断开连接
            }
        }
    }

    3. 设定绑定的IP地址

      一个主机可能有多个地址,此时可以显示指定

            ServerSocket serverSocket = new ServerSocket();
            // 只有在设定地址之前设置才有效
            serverSocket.setReuseAddress(true);
            serverSocket.bind(new InetSocketAddress(8000));    

    4. 关闭ServerSocket

      同样应该在finally代码块中调用close()方法,在一般的连接中,往往是由客户端发起请求,也是由客户端发起关闭socket请求。

      但是,在某些keepalive的场景中,例如httpd,nginx等等服务器都支持长连接,通过设定keepalive的最大连接时长和最大连接数来控制长连接。

      此时,那些由于超时的client连接,服务器端会主动发起close()请求。

      如何判断ServerSocket没有关闭

    boolean isOpen = serverSocket.isBound() && !serverSocket.isClosed();

    4.2 ServerSocket选项

    1. SO_TIMEOUT

      accept()方法等待客户端的连接超时时间,以ms为单位,0表示永不超时,默认是0.

      当执行accept()时,如果backlog为空,则服务器一直等待,如果设置了超时时间,则服务器端阻塞在此,超时则抛出SocketTimeoutException

    2. SO_REUSEADDR选项

      当服务器因为某些原因需要重启时,如果网络上还有发送到这个ServerSocket的数据,则ServerSocket不会立刻释放该端口,导致重启失败。

      设置为true的话可以确保释放,但是必须在绑定端口之前调用方法。

    3. SO_RCVBUF

      接收缓冲大小

    五、Demo

    import java.io.*;
    import java.net.*;
    import java.util.concurrent.*;
    
    public class EchoServer {
      private int port=8000;
      private ServerSocket serverSocket;
      private ExecutorService executorService; //线程池
      private final int POOL_SIZE=4;  //单个CPU时线程池中工作线程的数目
      
      private int portForShutdown=8001;  //用于监听关闭服务器命令的端口
      private ServerSocket serverSocketForShutdown;
      private boolean isShutdown=false; //服务器是否已经关闭
    
      private Thread shutdownThread=new Thread(){   //负责关闭服务器的线程
        public void start(){
          this.setDaemon(true);  //设置为守护线程(也称为后台线程)
          super.start();
        }
    
        public void run(){
          while (!isShutdown) {
            Socket socketForShutdown=null;
            try {
              socketForShutdown= serverSocketForShutdown.accept();
              BufferedReader br = new BufferedReader(
                                new InputStreamReader(socketForShutdown.getInputStream()));
              String command=br.readLine();
             if(command.equals("shutdown")){
                long beginTime=System.currentTimeMillis(); 
                socketForShutdown.getOutputStream().write("服务器正在关闭
    ".getBytes());
                isShutdown=true;
                //请求关闭线程池
    //线程池不再接收新的任务,但是会继续执行完工作队列中现有的任务
                executorService.shutdown();  
                
                //等待关闭线程池,每次等待的超时时间为30秒
                while(!executorService.isTerminated())
                  executorService.awaitTermination(30,TimeUnit.SECONDS); 
                
                serverSocket.close(); //关闭与EchoClient客户通信的ServerSocket 
                long endTime=System.currentTimeMillis(); 
                socketForShutdown.getOutputStream().write(("服务器已经关闭,"+
                    "关闭服务器用了"+(endTime-beginTime)+"毫秒
    ").getBytes());
                socketForShutdown.close();
                serverSocketForShutdown.close();
                
              }else{
                socketForShutdown.getOutputStream().write("错误的命令
    ".getBytes());
                socketForShutdown.close();
              }  
            }catch (Exception e) {
               e.printStackTrace();
            } 
          } 
        }
      };
    
      public EchoServer() throws IOException {
        serverSocket = new ServerSocket(port);
        serverSocket.setSoTimeout(60000); //设定等待客户连接的超过时间为60秒
        serverSocketForShutdown = new ServerSocket(portForShutdown);
    
        //创建线程池
        executorService= Executors.newFixedThreadPool( 
            Runtime.getRuntime().availableProcessors() * POOL_SIZE);
        
        shutdownThread.start(); //启动负责关闭服务器的线程
        System.out.println("服务器启动");
      }
      
      public void service() {
        while (!isShutdown) {
          Socket socket=null;
          try {
            socket = serverSocket.accept();  //可能会抛出SocketTimeoutException和SocketException
            socket.setSoTimeout(60000);  //把等待客户发送数据的超时时间设为60秒          
            executorService.execute(new Handler(socket));  //可能会抛出RejectedExecutionException
          }catch(SocketTimeoutException e){
             //不必处理等待客户连接时出现的超时异常
          }catch(RejectedExecutionException e){
             try{
               if(socket!=null)socket.close();
             }catch(IOException x){}
             return;
          }catch(SocketException e) {
             //如果是由于在执行serverSocket.accept()方法时,
             //ServerSocket被ShutdownThread线程关闭而导致的异常,就退出service()方法
             if(e.getMessage().indexOf("socket closed")!=-1)return;
           }catch(IOException e) {
             e.printStackTrace();
          }
        }
      }
    
      public static void main(String args[])throws IOException {
        new EchoServer().service();
      }
    }
    class Handler implements Runnable{
      private Socket socket;
      public Handler(Socket socket){
        this.socket=socket;
      }
      private PrintWriter getWriter(Socket socket)throws IOException{
        OutputStream socketOut = socket.getOutputStream();
        return new PrintWriter(socketOut,true);
      }
      private BufferedReader getReader(Socket socket)throws IOException{
        InputStream socketIn = socket.getInputStream();
        return new BufferedReader(new InputStreamReader(socketIn));
      }
      public String echo(String msg) {
        return "echo:" + msg;
      }
      public void run(){
        try {
          System.out.println("New connection accepted " +
          socket.getInetAddress() + ":" +socket.getPort());
          BufferedReader br =getReader(socket);
          PrintWriter pw = getWriter(socket);
    
          String msg = null;
          while ((msg = br.readLine()) != null) {
            System.out.println(msg);
            pw.println(echo(msg));
            if (msg.equals("bye"))
              break;
          }
        }catch (IOException e) {
           e.printStackTrace();
        }finally {
           try{
             if(socket!=null)socket.close();
           }catch (IOException e) {e.printStackTrace();}
        }
      }
    }
  • 相关阅读:
    第32章 数据库的备份和恢复
    Perl 打印关键字上下行
    mysql select * into OUTFILE 不会锁表
    独享表空间 ibdata1
    sql 使用单引号
    Oracle 维护常用SQL
    Mysql 独享表空间
    Mysql Perl unload表数据
    PLSQL 拼接SQL
    begin和declare
  • 原文地址:https://www.cnblogs.com/carl10086/p/6034563.html
Copyright © 2020-2023  润新知