网络编程 二 Socket通信上篇

编程入门 行业动态 更新时间:2024-10-22 18:29:49

<a href=https://www.elefans.com/category/jswz/34/1768814.html style=网络编程 二 Socket通信上篇"/>

网络编程 二 Socket通信上篇

文章目录

    • 一 Socket
    • 二 连接类型
    • 三 ServerSocket常用API
      • 3.1 构建服务端ServerSocket对象
        • 3.1.1 绑定服务端口
        • 3.1.2 等待连接超时设置
        • 3.1.3 限制可接受连接数量
        • 3.1.4 绑定IP地址
        • 3.1.5 后置绑定
      • 3.2 访问绑定信息
      • 3.3 超时Socket地址复用
      • 3.4 接收缓冲区大小
      • 3.5 其他API
    • 附录
      • RFC

一 Socket

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。(王雷,TCP/IP网络编程基础教程,北京理工大学出版社,2017.02,第4页)

  Socket不同于Http,Http是TPC/IP之上的高层协议,Socket不是协议,而是一种通信技术的实现,很多开发语言都对Socket提供了支持,可以说Java Socket是Java编程领域里的一个极为重量级的分支。

  非阻塞式的Socket应用,请大家参考另一个知识分享系列《NIO》。

二 连接类型

  常常能听到长连接、短连接,实际上两者唯一的区分在于此通信连接是否会长时间持有,如果一个通信过程结束后即断开连接,以后的每一次通信过程均创建新连接实现,那就是短链接。

  因此短链接用一次断一次,客户端和服务端可以约定好,当连接断开即可认为通信过程结束,此过程中的通信报文传输无误;而长连接因为一直保持着通信状态,所以需要客户端和服务端必须约定好报文的解析协议,即一次完整的通信报文是从什么位置开始的,又从什么位置结束,这是和短链接在编程实现角度上最大的不同点。

  总结一下长短连接的主要差异:

  1. 长连接仅在第一创建时需要执行握手协议(三次握手),后续较短链接节省了此过程,因此效率高;
  2. 短链接每次使用过后即断开,在服务端不需要保持大量的通信通道,相较于长连接系统资源消耗少(主要是内存占用率低);

  关于通信过程的协议实现部分(包括三次握手、四次挥手等等),网络上的资料非常多,如果有需要我后面会单独开一篇来介绍,本文的重点在于Socket通信实现相关的API使用,请读者朋友海涵。受篇幅限制,Socket通信相关API的介绍分上下两篇,本文仅介绍服务端ServerSocket部分。

三 ServerSocket常用API

  实际上服务端和客户端的TCP通信都是基于Socket对象实现的,但是服务端在构建Socket对象时需要通过ServerSocket对象实现,而ServerSocket提供了非常丰富的服务端通信配置,包括但不限于端口绑定、地址绑定、信息查询。

3.1 构建服务端ServerSocket对象

  构建ServerSocket对象的时候,就已经开始了配置绑定过程,除非使用了默认的无参构造函数,这种情况下需要开发者通过其他配置绑定API来完成通信参数的配置。

3.1.1 绑定服务端口

  服务端提供了ServerSocket类,它的构造方法允许传入一个整形参数,用以描述此服务的端口,客户端可通过此端口发起连接请求:

public class TestSocketServer {public static void main(String[] args) throws IOException {ServerSocket socketServer = new ServerSocket(8080);socketServer.accept();System.out.println("有客户端发起来连接请求,因此阻塞结束,执行到了这里");socketServer.close();}
}

  注意,accept方法是阻塞的,执行到此处后如果没有客户端发起连接请求,那么会持续阻塞;解除阻塞状态有几种途径,其一是客户端Socket对象发起连接请求,另一种是利用API设置阻塞超时时间,一旦限定时间内没有客户端发起连接请求阻塞解除,当然还可以利用Java的中断机制实现。

  accept方法如果因客户端的连接请求而结束中断,那么此方法会返回一个Socket对象,这是和客户端创建Socket对象的最大不同点,其他关于Socket的操作(读、写、关闭等)基本一致。

  另一个需要注意的点是端口号是有范围限制的,0 ~65536是有效端口范围,但不要使用操作系统内部服务的端口号(保留端口)以及常用服务的端口(比如说一些数据库和Web服务的常用端口1433,3306),并且当端口号被设置为0时,会随机分配一个可用端口号:

public class TestSocketServer {public static void main(String[] args) throws IOException {ServerSocket server = new ServerSocket(0);System.out.println(server.getLocalPort());server = new ServerSocket(8080);System.out.println(server.getLocalPort());}
}输出结果:
59395
8080
3.1.2 等待连接超时设置

  前文中提到了ServerSocket的accept方法会阻塞至客户端发起连接请求,解除阻塞的方法还包括设置超时时间,ServerSocket对象提供了setSoTimeout(int timeout)方法对accept进行超时设置(需要注意的是NIO并不提供连接超时设置,如通过SocketChannel.open()获得的套接字设置读取超时,read超时返回的是0,而不会抛出超时异常),参数单位是毫秒,超过参数限定时间后无连接请求则抛出异常,那么服务端可在异常处理中决定是否继续accept或者执行其他处理:

public class TestSocketServer {public static void main(String[] args) throws IOException {ServerSocket server = new ServerSocket(8080);server.setSoTimeout(2000);server.accept();}
}两秒后输出:
Exception in thread "main" java.SocketTimeoutException: Accept timed outat java.PlainSocketImpl.socketAccept(Native Method)at java.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)at java.ServerSocket.implAccept(ServerSocket.java:545)at java.ServerSocket.accept(ServerSocket.java:513)at com.demo.socket.TestSocketServer.main(TestSocketServer.java:10)
3.1.3 限制可接受连接数量

  服务端的资源永远是有限且宝贵的,尤其是基于TCP协议的长连接会在服务端留存大量的Socket连接,这对系统资源的控制造成了潜在威胁,ServerSocket类允许设置可接受的连接数量来控制资源消耗。

  ServerSocket提供了一个重载的构造函数,除设置服务端口外,还允许填入一个控制连接数量的参数值backlog,一旦可接受连接数超过此阈值就会抛出异常(注意,此异常由客户端处理,异常类型为Connection refused:connect):

public class TestSocketServer {public static void main(String[] args) throws IOException {ServerSocket server = new ServerSocket(8080, 5);server.accept();}
}public class TestSocketClient {public static void main(String[] args) throws IOException {for (int i = 0; i < 10; i++) {Socket socket = new Socket("127.0.0.1", 8080);}}
}客户端异常:
Exception in thread "main" java.ConnectException: Connection refused (Connection refused)at java.PlainSocketImpl.socketConnect(Native Method)at java.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)at java.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)at java.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)at java.SocksSocketImpl.connect(SocksSocketImpl.java:392)at java.Socket.connect(Socket.java:589)at java.Socket.connect(Socket.java:538)at java.Socket.<init>(Socket.java:434)at java.Socket.<init>(Socket.java:211)at com.demo.socket.TestSocketClient.main(TestSocketClient.java:9)

  最后需要提醒的是backlog参数默认值是50,而实际上能够接受多少个请求连接是由操作系统决定的,因为这个参数最后会通过本地方法和操作系统进行交互。

3.1.4 绑定IP地址

  一般情况下一个主机只有一个IP接入网络,但是并不排除存在某下主机安装有两块网卡,或者一块网卡支持多个IP设定的情况。这时候创建ServerSocket就必须显示的指定服务端访问IP。

  ServerSocket提供了一个重载构造函数,除指定端口、可接受连接数限制,还允许绑定服务端IP,假如我本机存在两块网卡,其一是内部局域网IP地址192.168.2.13,另一个是外网IP222.56.3.34,现在我的TCP服务端仅对内提供服务,创建ServerSocket对象时就可以按如下方式实现:

public class TestSocketServer {public static void main(String[] args) throws IOException {ServerSocket server = new ServerSocket(8080, 5, InetAddress.getByName("192.168.2.13"));server.accept();}
}
3.1.5 后置绑定

  前文介绍的IP、PORT配置都是通过构造函数实现的,ServerSocket还允许在创建实例(使用无参构造函数创建ServerSocket对象)后,通过配置API进行参数设置,配置API如下:

    public void bind(SocketAddress endpoint) throws IOException {bind(endpoint, 50);}public void bind(SocketAddress endpoint, int backlog) throws IOException {if (isClosed())throw new SocketException("Socket is closed");if (!oldImpl && isBound())throw new SocketException("Already bound");if (endpoint == null)endpoint = new InetSocketAddress(0);if (!(endpoint instanceof InetSocketAddress))throw new IllegalArgumentException("Unsupported address type");InetSocketAddress epoint = (InetSocketAddress) endpoint;if (epoint.isUnresolved())throw new SocketException("Unresolved address");if (backlog < 1)backlog = 50;try {SecurityManager security = System.getSecurityManager();if (security != null)security.checkListen(epoint.getPort());getImpl().bind(epoint.getAddress(), epoint.getPort());getImpl().listen(backlog);bound = true;} catch(SecurityException e) {bound = false;throw e;} catch(IOException e) {bound = false;throw e;}}

  bind方法提供了两个重载,后一个允许配置最大可接受的连接请求数。两个API的第一个参数都是SocketAddress,实际上这是一个抽象类:

public abstract class SocketAddress implements java.io.Serializable {static final long serialVersionUID = 5215720748342549866L;
}

  通过IDE工具可以直接查看到它的直接派生类是InetSocketAddress,InetSocketAddress提供了三个重载的构造函数供开发者使用,参数的应用场景与ServerSocket一致,这部分就不赘述了。

3.2 访问绑定信息

  通过ServerSocket对象可反向访问其绑定的IP、PORT等信息,访问过程通过ServerSocket对象提供的getLocalSocketAddress方法,获取到对应的SocketAddress对象,SocketAddress对象提拱了访问绑定的IP、PORT等信息,这里以前文介绍过的bind方法为例:

public class TestSocketServer {public static void main(String[] args) throws IOException {ServerSocket server = new ServerSocket();server.bind(new InetSocketAddress("127.0.0.1", 8080), 12);System.out.println(server.getLocalPort());System.out.println(server.getSoTimeout());// 注意InetAddress和InetSocketAddress的区别InetAddress inetAddress = server.getInetAddress();System.out.println(inetAddress.getHostAddress());System.out.println(inetAddress.getHostName());InetSocketAddress inetSocketAddress = (InetSocketAddress)server.getLocalSocketAddress();System.out.println(inetSocketAddress.getHostName());System.out.println(inetSocketAddress.getHostString());// 看一下InetSocketAddress返回的InetAddress对象,和ServerSocket返回的InetAddress对象是不是同一个System.out.println(inetAddress.hashCode());System.out.println(inetSocketAddress.getAddress().hashCode());}
}输出结果:
8080
0
127.0.0.1
localhost
localhost
localhost
2130706433
2130706433

  上面的示例中有非常多的细节,首先bind方法常用于使用无参构造的ServerSocket对象创建之后,进行参数绑定。其次InetAddress可通过SocketServer对象获取,也可以通过InetSocketAddress对象获取,两者是相同的引用。最后需要稍微区分一下InetAddress和InetSocketAddress,前者是IP地址的抽象描述没,后者是Socket地址的抽象描述。

  当然还有一点比较重要但是容易被忽略的,InetSocketAddress提供了两个API用于访问主机名:

    /*** Gets the {@code hostname}.* Note: This method may trigger a name service reverse lookup if the* address was created with a literal IP address.** @return  the hostname part of the address.*/public final String getHostName() {return holder.getHostName();}/*** Returns the hostname, or the String form of the address if it* doesn't have a hostname (it was created using a literal).* This has the benefit of <b>not</b> attempting a reverse lookup.** @return the hostname, or String representation of the address.* @since 1.7*/public final String getHostString() {return holder.getHostString();}

  仔细阅读源码注释,会发现如果地址信息是通过IP字符串创建的,那么getHostName方法会通过DNS服务器反向查找主机名,而getHostString则不会,如果尚未发现主机名则返回IP地址信息,这里需要通过下面的示例,并结合源码进行分析,先看示例:

public class TestSocketServer {public static void main(String[] args) throws IOException {InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.1", 8080);InetSocketAddress inetSocketAddress2 = new InetSocketAddress("127.0.0.1", 8080);System.out.println(inetSocketAddress1.getHostName());System.out.println(inetSocketAddress2.getHostString());}
}输出结果:
localhost
127.0.0.1

  两个InetSocketAddress对象的信息应该是一致的,但是getHostName触发了DNS的反向查找,现在回过头来仔细看我前文的说明,getHostString方法执行时如果尚未查询到主机名,会直接打印IP信息,那么怎么才能验证呢?很简单,用相同的InetSocketAddress对象执行两个方法即可:

public class TestSocketServer {public static void main(String[] args) throws IOException {InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.1", 8080);System.out.println(inetSocketAddress1.getHostName());System.out.println(inetSocketAddress1.getHostString());}
}输出结果:
localhost
localhost

  这个现象从InetSocketAddress的源码中也可以看的出来:

	/*** InetSocketAddress的getHostString方法,holder是InetSocketAddress的私有静态内部类对象*/ public final String getHostString() {return holder.getHostString();}/*** InetSocketAddressHolder*/private String getHostString() {if (hostname != null)return hostname;if (addr != null) {if (addr.holder().getHostName() != null)return addr.holder().getHostName();elsereturn addr.getHostAddress();}return null;}

  如果holder的hostName属性不为空,那么返回hostname值,否则返回hostAdderess。

3.3 超时Socket地址复用

  这个问题有些复杂,做过通信优化的朋友可能理解起来较为容易。当我们关闭TCP连接的时候,此连接可能在很短的一段时间内都处于“TIME_WAIT”状态,与之对应的还有一个状态叫“CLOSE_WAIT”,当然这里不细说其他状态。

  当连接状态为“TIME_WAIT”时,操作系统会在一定时间内将其回收掉,以便其对应的Socket地址可以在后续阶段继续使用,但如果在“TIME_WAIT”状态期间,此连接对应Socket地址是无法被继续使用的,某些场景下对服务端来说是难以接受的。

  优化“TIME_WAIT”状态的手段也比较多,从操作系统层面入手可以,通过ServerSocket提供的setReuseAddress方法也可以:

public void setReuseAddress(boolean on)

  但有一点一定要注意,是否启用超时Socket地址复用需要看操作系统是否支持,据了解Centos是支持的,Windows因为没有完全实现BSD Socket标准,因此可能不支持,实际情况读者可自行测试验证。

  对应的ServerSocket提供了下述方式查询此设置是否已开启:

public boolean getReuseAddress()

3.4 接收缓冲区大小

  这是一个涉及TCP标准实现的API设定,ServerSocket和Socket对象均提供设置和访问接收缓冲区大小的API:

public void setReceiveBufferSize(int size);
public int getReceiveBufferSize();

  这里给大家看一下源码中的注释:

/*** Sets a default proposed value for the* {@link SocketOptions#SO_RCVBUF SO_RCVBUF} option for sockets* accepted from this {@code ServerSocket}. The value actually set* in the accepted socket must be determined by calling* {@link Socket#getReceiveBufferSize()} after the socket* is returned by {@link #accept()}.* <p>* The value of {@link SocketOptions#SO_RCVBUF SO_RCVBUF} is used both to* set the size of the internal socket receive buffer, and to set the size* of the TCP receive window that is advertized to the remote peer.* <p>* It is possible to change the value subsequently, by calling* {@link Socket#setReceiveBufferSize(int)}. However, if the application* wishes to allow a receive window larger than 64K bytes, as defined by RFC1323* then the proposed value must be set in the ServerSocket <B>before</B>* it is bound to a local address. This implies, that the ServerSocket must be* created with the no-argument constructor, then setReceiveBufferSize() must* be called and lastly the ServerSocket is bound to an address by calling bind().* <p>* Failure to do this will not cause an error, and the buffer size may be set to the* requested value but the TCP receive window in sockets accepted from* this ServerSocket will be no larger than 64K bytes.** @exception SocketException if there is an error* in the underlying protocol, such as a TCP error.** @param size the size to which to set the receive buffer* size. This value must be greater than 0.** @exception IllegalArgumentException if the* value is 0 or is negative.** @since 1.4* @see #getReceiveBufferSize*/public synchronized void setReceiveBufferSize (int size) throws SocketException

  注释中描述了几个比较重的点,此API是用来设置Socket的SO_RCVBUF的建议值的,如果应用程序想设置大于RCF1323中约定的64KB的接收窗口的缓存值,那么必须在ServerSocket绑定到SocketAddress之前进行设置。(RFC1323请参阅附录)

  这意味着,如果服务端需要设置接收缓冲区大小,必须按照如下顺序编写程序:

  1. 使用ServerSocket无参构造函数创建对象
  2. 使用setReceiveBufferSize设置建议值
  3. 使用bind函数进行SocketAddress绑定

  客户端Socket则必须在发起连接动作之前进行设置,否则参数设置无效,这样也不会抛出异常(但是参数值小于零会抛出异常),只不过TCP接收窗口的大小不会大于64KB而已。以服务端为例:

public class TestSocketServer {public static void main(String[] args) throws IOException {ServerSocket serverSocket = new ServerSocket();System.out.println("此前值:" + serverSocket.getReceiveBufferSize());serverSocket.setReceiveBufferSize(3000);serverSocket.bind(new InetSocketAddress("127.0.0.1", 8080), 50);serverSocket.accept();System.out.println("此后值:" + serverSocket.getReceiveBufferSize());}
}输出结果:
此前值:131072
此后值:3000

  我本机在进行测试的时候,如果此建议值设置小于1024,那么建议值不会采纳,依旧为1024。而且我从其他途经获取到的资料都说默认的接收缓冲区大小为8KB即8192,但我本机(MAC OS)实际测试结果是131072,我猜测和操作系统有关。我本机装了Windows和Centos的虚拟机,但实在懒得测试,如果有朋友知道的这个问题的可以留言我,感激不尽。

3.5 其他API

  ServerSocket还提供了一些用于判定某些状态的简单API,这些API的使用较为简单,我以列表的方式整理,如果有遗漏的API请读者朋友参考其他媒体资源,或者通过JDK文档、源码等途经了解。

方法使用场景说明
boolean isBound()判定ServerSocket是否绑定了地址 信息
InetAddress getInetAddress()获取地址信息

附录

RFC

Request For Comments(RFC),是一系列以编号排定的文件。文件收集了有关互联网相关信息,以及UNIX和互联网社区的软件文件。RFC文件是由Internet Society(ISOC)赞助发行。基本的互联网通信协议都有在RFC文件内详细说明。RFC文件还额外加入许多在标准内的论题,例如对于互联网新开发的协议及发展中所有的记录。因此几乎所有的互联网标准都有收录在RFC文件之中。百度百科

  RFC是一种以序号编排的方案/标准讨论内容的文件记录,如果想精通TCP设计的话,可能需要大量阅读RFC文档,这里列一些从网络上找到的和TCP相关的文档记录:TCP协议经典rfc主题总结

更多推荐

网络编程 二 Socket通信上篇

本文发布于:2024-02-26 16:02:59,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1703074.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:网络编程   上篇   通信   Socket

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!