海码充电站的技术专栏 java Coder

网络(二)--SOCKET套接字

2019-01-17
watermelon


SOCKET套接字

前言

Socket套接字在TCP/IP中扮演着非常重要的角色。

1:为什么

为了搞清楚为什么socket对tpcip协议意义非凡,它是怎么工作的,到底是怎么发挥作用。

2:是什么

socket是BSD UNIX的通信机制,通常称为“套接字”,其英文原意是“孔”或“插座”。有些顾名思义,socket正如其英文原意一样,像是一个多孔插座,可以提供多个端口的连接服务。 Socket其实就是一个门面模式,它把复杂的TCP/IP(里面内容很多,包含TCP协议、UDP协议)协议族隐藏在Socket接口后面(程序员的视角:socket是对TPC/IP协议的封装,用起来更方便),对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。 Socket本身不算是协议,它只是提供了一个针对TCP或者UDP编程的接口。

什是socket

  • 像多孔插座

如果说socket是一个多孔插座,插座是提供各种电器供电的地方,不同的电器工作时需要的电压和电流也不一样,但各种电器都有各自的一个插口,这个称之为“端口”。
电器使用的电可以看做是网络资源或者是各种“流”,电是由电线传输过来的,所以插座需要连接电线,这里电线也就是服务器和客户端连接 “connection”。
其中初始化socket的过程像是买来一个插座的安装过程。 在插座这边的是“客户端”,电线那边提供电的发电厂是“服务器”。
客户端和发电厂都各自拥有一个地址,即“IP地址”。
其中还有一套传输和用电的规则,比如传输电时需要的电线多少伏才能满足需求,电器用电的技术参数,端口是几个孔的。这个是“协议”。
正常情况下我们是不会去管协议的内容是什么,也就是说协议在我们面前是隐藏的。

  • 工作原理

对于服务器来说,服务器先初始化socket,然后端口绑定(bind),再对端口监听(listen),调用accept阻塞,等待客户端连接请求。对于客户端来说,客户端初始化socket,然后申请连接(connection)。客户端申请连接,服务器接受申请并且回复申请许可(这里要涉及TCP三次握手连接),然后发送数据,最后关闭连接,这是一次交互过程

  • Socket和NioSocket
    Java中的Socket分为普通的Socket和NioSocket。
普通的Socket:

ServerSocket用于服务器端,可以通过accept方法监听请求,监听请求后返回Socket,Socket用于完成具体数据传输,客户端也可以使用Socket发起请求并传输数据。ServerSocket的使用可以分为三步:

1:创建ServerSocket。ServerSocket的构造方法有5个,其中最方便的是ServerSocket(int port),只需要一个port就可以了。 2:调用创建出来的ServerSocket的accept方法进行监听。accept方法是阻塞方法,也就是说调用accept方法后程序会停下来等待连接请求,在接受请求之前程序将不会继续执行,当接收到请求后accept方法返回一个Socket。
3:使用accept方法返回的Socket与客户端进行通信

NioSocket:

从JDK1.4开始,Java增加了新的IO模式-nio(new IO),nio在底层采用了新的处理方式,极大提高了IO的效率。我们使用的Socket也是IO的一种,nio提供了相应的工具:ServerSocketChannel和SocketChannel,他们分别对应原来的ServerSocket和Socket。
服务器端NioSocket的处理过程:

1:创建ServerSocketChannel并设置相应的端口号、是否为阻塞模式 2:创建Selector并注册到ServerSocketChannel上 3:调用Selector的selector方法等待请求 4:Selector接收到请求后使用selectdKeys返回SelectionKey集合 5:使用SelectionKey获取到channel、selector和操作类型并进行具体操作。

3:怎么用

普通的Socket

public class SocketConstants {
    //主机地址
    public static final String HOST_ADDR = "127.0.0.1";

    //端口号
    public static final int PORT = 10003;
}
  • 服务端:
    public class TcpServer {
      public static void main(String[] args) throws Exception {
          //建立服务端socket服务,并监听一个port
          ServerSocket ss = new ServerSocket(PORT);
          //通过accept方法获取连接过来的client对象
          Socket s = ss.accept();
          String ip = s.getInetAddress().getHostAddress();
          //获取client发送过来的数据。使用client对象的读取流来读取数据
          InputStream in = s.getInputStream();
    
          byte[] buf = new byte[1024];
          int len = in.read(buf);
          System.out.println(new String(buf,0,len));
          //关闭client
          s.close();
      }
    }
    
  • 客户端:
    public class TcpClient {
      public static void main(String[] args) throws Exception {
          //创建client的socket服务,指定目的主机和port
          Socket s = new Socket(HOST_ADDR,PORT);
          //为了发送数据。获取socket流中的输出流
          java.io.OutputStream out = s.getOutputStream();
          out.write("hello tcp".getBytes());
          s.close();
      }
    }
    

NioSocket:

public class NIOServer {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        try {
            //创建ServerSocketChannel,监听8080端口
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress(8080));
            //设置为非阻塞模式
            ssc.configureBlocking(false);
            //为ssc注册选择器
            Selector selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            //创建处理器
            Handler handler = new Handler(1024);
            while(true){
                //等待请求,每次等待阻塞3s,超过3s后线程继续向下运行,如果传入0或者不传入参数则一直阻塞
                if(selector.select(3000) == 0){
                    System.out.println("等待请求超时----");
                    continue;
                }
                System.out.println("处理请求----");
                //获取处理的SelectionKey
                Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
                while(keyIter.hasNext()){
                    SelectionKey key = keyIter.next();
                    try{
                        //接收到连接请求时
                        if(key.isAcceptable()){
                            handler.handleAccept(key);
                        }
                        //读数据
                        if(key.isReadable()){
                            handler.handleRead(key);
                        }
                    }catch(IOException ex){
                        keyIter.remove();
                        continue;
                    }
                    //处理完后,从待处理的SelectionKey迭代器中移除当前所使用的key
                    keyIter.remove();
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    private static class Handler{
        private int bufferSize = 1024;
        private String localCharset = "UTF-8";
        public Handler(int bufferSize){
            this.bufferSize = bufferSize;
        }
        public void handleAccept(SelectionKey key) throws IOException{
            SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
            sc.configureBlocking(false);
            sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
        }
        public void handleRead(SelectionKey key) throws IOException{
            //获取Channel
            SocketChannel sc = (SocketChannel) key.channel();
            //获取buffer并重置
            ByteBuffer buffer = (ByteBuffer)key.attachment();
            buffer.clear();
            //没有读到内容则关闭
            if(sc.read(buffer) == -1)
                sc.close();
            else{
                //将buffer转换为读状态
                buffer.flip();
                //将buffer中接收到的值按localCharset格式编码后保存到receivedString
                String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
                System.out.println("received from client:" + receivedString);
                //返回数据给客户端
                String sendString = "this data is from Server";
                buffer = ByteBuffer.wrap(sendString.getBytes(localCharset));
                sc.write(buffer);
                sc.close();
            }
        }
    }
}

//客户端代码通普通Socket一样,Socket socket = new Socket("192.168.6.42",8080)

4:跟http的区别

  • 区别1:socket是接口,致力于传输层的套接字;http是应用层协议(可能区别本身应该描述成:传输层与应用层的区别更合适)
  • 区别2:socket是长连接(有些场景需要配合高速轮训机制保持连接处于活跃性);http是短连接

http 为短连接:客户端发送请求都需要服务器端回送响应.请求结束后,主动释放链接,因此为短连接。通常的做法是,不需要任何数据,也要保持每隔一段时间向服务器发送”保持连接”的请求。这样可以保证客户端在服务器端是”上线”状态。 HTTP连接使用的是”请求-响应”方式,不仅在请求时建立连接,而且客户端向服务器端请求后,服务器才返回数据。

Socket为长连接:通常情况下Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通讯双方开始互发数据内容,直到双方断开连接。在实际应用中,由于网络节点过多,在传输过程中,会被节点断开连接,因此要通过轮询高速网络,该节点处于活跃状态。

5:跟TCP和UDP的关系

socket编程有两种通信协议可以进行选择。一种是数据报通信,另一种就是流通信 。(前者就是UDP通信,后者是TCP通信。)

UDP是一种无连接的协议,这就意味着我们每次发送数据报时,需要同时发送本机的socket描述符和接收端的socket描述符。
因此,我们在每次通信时都需要发送额外的数据。
UDP用来实现实时性较高或者丢包不重要的一些服务。在局域网中UDP的丢包率都相对比较低。比如语音、视频聊天等。

TCP是一种基于连接的协议。在使用流通信之前,我们必须在通信的一对儿socket之间建立连接。其中一个socket作为服务器进行监听连接请求。另一个则作为客户端进行连接请求。一旦两个socket建立好了连接,他们可以单向或双向进行数据传输。
TCP适合于诸如远程登录(rlogin,telnet)和文件传输(FTP)这类的网络服务。

Socket选择可以指定Socket类发送和接受数据的方式。在JDK1.4中共有8个Socket选择可以设置。这8个选项都定义在java.net.SocketOptions接口中。定义如下:

public final static int TCP_NODELAY = 0x0001;
public final static int SO_REUSEADDR = 0x04;
public final static int SO_LINGER = 0x0080;
public final static int SO_TIMEOUT = 0x1006;
public final static int SO_SNDBUF = 0x1001;
public final static int SO_RCVBUF = 0x1002;
public final static int SO_KEEPALIVE = 0x0008;
public final static int SO_OOBINLINE = 0x1003;

有趣的是,这8个选项除了第一个没在SO前缀外,其他7个选项都以SO作为前缀。
其实这个SO就是Socket Option的缩写;因此,在Java中约定所有以SO为前缀的常量都表示Socket选项;
当然,也有例外,如TCP_NODELAY.在Socket类中为每一个选项提供了一对get和set方法,分别用来获得和设置这些选项。

引用:


Comments

Content