首页 > 代码库 > Java Socket编程基础(1)

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=http://www.mamicode.com/br.readLine())!=null){>*/        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();}    }  }}

 

Java Socket编程基础(1)