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\r\n");
        sb.append("Host: www.javathinker.org\r\n");
        sb.append("Accept: */*\r\n");
        sb.append("Accept-Language: zh-cn\r\n");
        sb.append("Accept-Encoding: gzip, deflate\r\n");
        sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n");
        sb.append("Connection: Keep-Alive\r\n\r\n");

        //发出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\r\n");
        sb.append("Host: www.javathinker.org\r\n");
        sb.append("Accept: */*\r\n");
        sb.append("Accept-Language: zh-cn\r\n");
        sb.append("Accept-Encoding: gzip, deflate\r\n");
        sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n");
        sb.append("Connection: Keep-Alive\r\n\r\n");

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

(1) 如果是字符流,可以以特殊字符作为结束标志,可以是\r\n\r\n,甚至于可以定义为"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("服务器正在关闭\r\n".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)+"毫秒\r\n").getBytes());
            socketForShutdown.close();
            serverSocketForShutdown.close();
            
          }else{
            socketForShutdown.getOutputStream().write("错误的命令\r\n".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();}
    }
  }
}

 

posted @ 2016-11-06 15:21  carl_ysz  阅读(2174)  评论(0编辑  收藏  举报