首先必须明确:TCP/IP模型中有四层结构:
     
应用层(Application Layer)、传输层(Transport 
Layer)、网络层(Internet Layer  )、链路层(LinkLayer)
 其中Ip协议(Internet
Protocol)是位于网络层的,TCP协议时位于传输层的。通过Ip协议可以使可以使两台计算机使用同一种语言,从而允许Internet上连接不同类型的计算机和不同操作系统的网络。Ip协议只保证计算机能够接收和发送分组数据。
当计算机要和远程的计算机建立连接时,TCP协议会让他们建立连接:用于发送和接收数据的虚拟电路。

在JAVA中,我们用 ServerSocket、Socket类创建一个套接字连接,从套接字得到的结果是一个InputStream以及OutputStream对象,以便将连接作为一个IO流对象对待。通过IO流可以从流中读取数据或者写数据到流中,读写IO流会有异常IOException产生。

套接字或插座(socket)是一种软件形
式的抽象,用于表达两台机器间一个连接的“终端”。针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。JAVA
有两个基于数据流的套接字类:ServerSocket,服务器用它“侦听”进入的连接;Socket,客户端用它初始一次连接。侦听套接字只能接收新的
连接请求,不能接收实际的数据包,即ServerSocket不能接收实际的数据包。

套接字是基于TCP/IP实现的,它是用来提供一个访问TCP的服务接口,或者说套接字socket是TCP的应用编程接口API,通过它应用层就可以访问TCP提供的服务。

在JAVA中,我们用
ServerSocket、Socket类创建一个套接字连接,从套接字得到的结果是一个InputStream以及OutputStream对象,以便
将连接作为一个IO流对象对待。通过IO流可以从流中读取数据或者写数据到流中,读写IO流会有异常IOException产生。

套接字底层是基于TCP的,所以socket的超时和TCP超时是相同的。下面先讨论套接字读写缓冲区,接着讨论连接建立超时、读写超时以及JAVA套接字编程的嵌套异常捕获和一个超时例子程序的抓包示例。

1
socket读写缓冲区

一旦创建了一个套接字实例,操作系统就会为其分配缓冲区以存放接收和要发送的数据。

JAVA可以设置读写缓冲区的大小-setReceiveBufferSize(int size),
setSendBufferSize(int size)。

向输出流写数据并不意味着数据实际上已经被发送,它们只是被复制到了发送缓冲区队列SendQ,就是在Socket的OutputStream上调用
flush()方法,也不能保证数据能够立即发送到网络。真正的数据发送是由操作系统的TCP协议栈模块从缓冲区中取数据发送到网络来完成的。

当有数据从网络来到时,TCP协议栈模块接收数据并放入接收缓冲区队列RecvQ,输入流InputStream通过read方法从RecvQ中取出数据。

2 socket连接建立超时

socket连接建立是基于TCP的连接建立过程。TCP的连接需要通过3次握手报文来完成,开始建立TCP连接时需要发送同步SYN报文,然后等待确认
报文SYN+ACK,最后再发送确认报文ACK。TCP连接的关闭通过4次挥手来完成,主动关闭TCP连接的一方发送FIN报文,等待对方的确认报文;被
动关闭的一方也发送FIN报文,然等待确认报文。

正在等待TCP连接请求的一端有一个固定长度的连接队列,该队列中的连接已经被TCP接受(即三次握手已经完成),但还没有被应用层所接受。TCP接受一个连接是将其放入这个连接队列,而应用层接受连接是将其从该队列中移出。应用层可以通过设置backlog变量来指明该连接队列的最大长度,即已被TCP接受而等待应用层接受的最大连接数。

当一个连接请求SYN到达时,TCP确定是否接受这个连接。如果队列中还有空间,TCP模块将对SYN进行确认并完成连接的建立。但应用层只有在三次握手中的第三个报文收到后才会知道这个新连接。如果队列没有空间,TCP将不理会收到的SYN。

如果应用层不能及时接受已被TCP接受的连接,这些连接可能占满整个连接队列,新的连接请求可能不被响应而会超时。如果一个连接请求SYN发送后,一段时间后没有收到确认SYN+ACK,TCP会重传这个连接请求SYN两次,每次重传的时间间隔加倍,在规定的时间内仍没有收到SYN+ACK,TCP将放弃这个连接请求,连接建立就超时了。

JAVA
Socket连接建立超时和TCP是相同的,如果TCP建立连接时三次握手超时,那么导致Socket连接建立也就超时了。可以设置Socket连接建立的超时时间-

connect(SocketAddress endpoint, int
timeout)

如果在timeout内,连接没有建立成功,在TimeoutException异常被抛出。如果timeout的值小于三次握手的时间,那么Socket连接永远也不会建立。

不同的应用层有不同的连接建立过程,Socket的连接建立和TCP一样-仅仅需要三次握手就完成连接,但有些应用程序需要交互很多信息后才能成功建立连接,比如Telnet协议,在TCP三次握手完成后,需要进行选项协商之后,Telnet连接才建立完成。

3 socket读超时

如果输入缓冲队列RecvQ中没有数据,read操作会一直阻塞而挂起线程,直到有新的数据到来或者有异常产生。调用setSoTimeout(int
timeout)可以设置超时时间,如果到了超时时间仍没有数据,read会抛出一个SocketTimeoutException,程序需要捕获这个异
常,但是当前的socket连接仍然是有效的。

如果对方进程崩溃、对方机器突然重启、网络断开,本端的read会一直阻塞下去(由前面可知:双方要关闭连接需要四次挥手
.对方机重启或断开只是对方机的TCP连接关闭,本端的TCP连接还没关闭,所以本端机会一直阻塞),这时设置超时时间是非常重要的,否则调用read的线程会一直挂起。

TCP模块把接收到的数据放入RecvQ中,直到应用层调用输入流的read方法来读取。如果RecvQ队列被填满了,这时TCP会根据滑动窗口机制通知
对方不要继续发送数据,本端停止接收从对端发送来的数据,直到接收者应用程序调用输入流的read方法后腾出了空间。

4 socket写超时

socket的写超时是基于TCP的超时重传。超时重传是TCP保证数据可靠性传输的一个重要机制,其原理是在发送一个数据报文后就开启一个计时器,在一
定时间内如果没有得到发送报文的确认ACK,那么就重新发送报文。如果重新发送多次之后,仍没有确认报文,就发送一个复位报文RST,然后关闭TCP连
接。首次数据报文发送与复位报文传输之间的时间差大约为9分钟,也就是说如果9分钟内没有得到确认报文,就关闭连接。但是这个值是根据不同的TCP协议栈
实现而不同。

如果发送端调用write持续地写出数据,直到SendQ队列被填满。如果在SendQ队列已满时调用write方法,则write将被阻塞,直到
SendQ有新的空闲空间为止,也就是说直到一些字节传输到了接收者套接字的RecvQ中。如果此时RecvQ队列也已经被填满,所有操作都将停止,直到
接收端调用read方法将一些字节传输到应用程序。

当Socket的write发送数据时,如果网线断开、对端进程崩溃或者对端机器重启动,(由前面可知:双方要关闭连接需要四次挥手
.对端进程崩溃或者对端机器重启动只是对方机的TCP连接关闭,本端的TCP连接还没关闭,所以本端机会一直阻塞)TCP模块会重传数据,最后超时而关闭连接。下次如再调用write会导致一个异常而退出。

Socket写超时是基于TCP协议栈的超时重传机制,一般不需要设置write的超时时间,也没有提供这种方法。

5 双重嵌套异常捕获

如果ServerSocket、Socket构造失败,只需要仅仅捕获这个构造失败异常而不需要调用套接字的close方法来释放资源(必须保证构造失败
后不会留下任何需要清除的资源),因为这时套接字内部资源没有被成功分配。如果构造成功,必须进入一个try
finally语句块里调用close释放套接字。请参照下面例子程序。

import java.net.*;
import java.io.*;
public class SocketClientTest
{
  public static final int PORT = 8088;
  public static void main( String[] args ) throws
Exception
  {
    InetAddress
addr = InetAddress.getByName( "127.0.0.1" );
    Socket
socket = new Socket();
    try
    {
     
socket.connect( new InetSocketAddress( addr, PORT ), 30000 );
     
socket.setSendBufferSize(100);

BufferedWriter out = new BufferedWriter( new OutputStreamWriter(
socket.getOutputStream() ) );
     
int i = 0;

while( true )
     
{
       
System.out.println( "client sent --- hello *** " + i++ );
       
out.write( "client sent --- hello *** " + i );
       
out.flush();

Thread.sleep( 1000 );
     
}
    }
   
finally
    {
     
socket.close();
    }
  }
}

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServerTest
{
  public static final int PORT = 8088;
  public static final int BACKLOG = 2;
  public static void main( String[] args ) throws
IOException
  {
    ServerSocket
server = new ServerSocket( PORT, BACKLOG );
   
System.out.println("started: " + server);
    try
    {
     
Socket socket = server.accept();
     
try
     
{
       
BufferedReader in = new BufferedReader( new InputStreamReader(
socket.getInputStream() ) );
       
String info = null;

while( ( info = in.readLine() ) != null )
       
{
         
System.out.println( info );
       
}
     
}
     
finally
     
{
       
socket.close();
     
}
    }
   
finally
    {
     
server.close();
    }
  }
}

执行上面的程序,在程序运行一会儿之后,断开client和server之间的网络连接,在机器上输出如下:

Server上的输出:

Echoing:client sent
-----hello0

Echoing:client sent
-----hello1

Echoing:client sent
-----hello2

Echoing:client sent
-----hello3

Echoing:client sent
-----hello4

Echoing:client sent
-----hello5

Echoing:client sent
-----hello6

---->>
断开了网络连接之后没有数据输出

Client上的输出:

socket default timeout
= 0

socket =
Socket[addr=/10.15.9.99,port=8088,localport=4691]

begin to
read

client sent --- hello
*** 0

client sent --- hello
*** 1

client sent --- hello
*** 2

client sent --- hello
*** 3

client sent --- hello
*** 4

client sent --- hello
*** 5

client sent --- hello
*** 6

client sent --- hello
*** 7

client sent --- hello
*** 8

client sent --- hello
*** 9

client sent --- hello
*** 10

---->> 断开网络连接后客户端进程挂起

java.net.SocketException
: Connection reset by peer: socket write error

at
java.net.SocketOutputStream.socketWrite0(
Native Method
)

at
java.net.SocketOutputStream.socketWrite(
SocketOutputStream.java:92
)

at
java.net.SocketOutputStream.write(
SocketOutputStream.java:136
)

at
sun.nio.cs.StreamEncoder.writeBytes(
StreamEncoder.java:202
)

at
sun.nio.cs.StreamEncoder.implFlushBuffer(
StreamEncoder.java:272
)

at
sun.nio.cs.StreamEncoder.implFlush(
StreamEncoder.java:276
)

at
sun.nio.cs.StreamEncoder.flush(
StreamEncoder.java:122
)

at
java.io.OutputStreamWriter.flush(
OutputStreamWriter.java:212
)

at
java.io.BufferedWriter.flush(
BufferedWriter.java:236
)

at
com.xtera.view.SocketClientTest.main(
SocketClientTest.java:99
)

当hello6被发送到server端后,网络连接被断开,这时server端不能接收任何数据而挂起。client端仍然继续发送数据,实际上
hello7、hello8、hello9、hello10都被复制到SendQ队列中,write方法立即返回。当client的SendQ队列被填满
之后,write方法就被阻塞。TCP模块在发送报文hello7之后,没有收到确认而超时重传,再重传几次之后关闭了TCP连接,同时导致被阻塞的
write方法异常返回。

通过抓包工具,我们可以看到超时重传的报文。

下面是规范代码实例:(服务端和客户端实现双向(可通过键盘输入)交互通信)

public class JabberClient {

public static void main(String[] args)
   
     
throws IOException {
   
    // Passing
null to getByName() produces the
   
    // special
"Local Loopback" IP address, for
   
    // testing
on one machine w/o a network:
   
    InetAddress
addr =
   
     
InetAddress.getByName("127.0.0.1");
   
    //
Alternatively, you can use
   
    // the
address or name:
   
    //
InetAddress addr =
   
   
//   
InetAddress.getByName("127.0.0.1");
   
    //
InetAddress addr =
   
   
//   
InetAddress.getByName("localhost");
   
   
System.out.println("addr = " + addr);
   
    Socket
socket =
   
     
new Socket(addr, JabberServer.PORT);
   
    // Guard everything in a try-finally to make
   
    // sure that
the socket is closed:
   
    try {
   
     
System.out.println("socket = " + socket);
   
     
BufferedReader KeyIn = new BufferedReader(new
InputStreamReader(System.in));
   
     
BufferedReader in =
   
       
new BufferedReader(
   
         
new InputStreamReader(
   
           
socket.getInputStream()));
   
     
// Output is
automatically flushed
   
     
// by PrintWriter:
   
     
PrintWriter out =
   
       
new PrintWriter(
   
         
new BufferedWriter(
   
           
new OutputStreamWriter(
   
             
socket.getOutputStream())),true);
//   
     
for(int i = 0; i < 10; i ++) {
//   
       
out.println("howdy " + i);
//   
       
String str = in.readLine();
//   
       
System.out.println(str);
//   
     
}
//   
     
out.println("END");
   
     
String str =null;
   
     
while(true)
   
     
{
   
   
   
  str = KeyIn.readLine();
   
   
   
  if("END".equals(str))
   
   
   
   
  break;
   
   
   
  out.println(str);
   
   
   
 
System.out.println("Server:"+in.readLine());
   
   
   
 
   
   
   
 
   
     
}
   
    } finally
{
   
     
System.out.println("closing...");
   
     
socket.close();
   
    }
   
  }
    }
///:~

public class JabberServer { 
   
  // Choose a port outside of the range
1-1024:
   
  public static final int PORT = 8088;
   
  public static void main(String[] args)
   
     
throws IOException {
   
    ServerSocket
s = new ServerSocket(PORT);
   
   
System.out.println("Started: " + s);
   
    try {
   
     
Socket socket = s.accept();
   
     
try {
   
       
System.out.println(
   
         
"Connection accepted: "+ socket);
   
       
BufferedReader KeyIn = new BufferedReader(new
InputStreamReader(System.in));

BufferedReader in =
   
         
new BufferedReader(
   
           
new InputStreamReader(
   
             
socket.getInputStream()));
   
       
// Output is automatically flushed
   
       
// by PrintWriter:
   
       
PrintWriter out =
   
         
new PrintWriter(
   
           
new BufferedWriter(
   
             
new OutputStreamWriter(
   
               
socket.getOutputStream())),true);
   
       
while (true) { 
   
         
String str = in.readLine();
   
         
if (str.equals("END")) break;
   
         
System.out.println("Client: " + str);
   
         
out.println(KeyIn.readLine());

}
   
      // Always close the
two sockets...
   
     
} finally {
   
       
System.out.println("closing...");
   
       
socket.close();
   
     
}
   
    } finally
{
   
     
s.close();
   
    }
   
  }
    }
///:~

注意:一般在传输字符信息(例如txt文件,聊天信息)使用bufferedRead,printWrite 
,但是在传送一些文件时一定要使用字节输入输出流socket.getInputStream  
和scoket.getOutputStream(但是
使用上面的字符流传输文件时,当文件传送完毕时打开会报错!!!)

JAVA Socket 底层是怎样基于TCP/IP 实现的???的更多相关文章

  1. Socket: Java Socket 几个重要的TCP/IP选项解析(转)

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

  2. JAVA基础知识之网络编程——-TCP/IP协议,socket通信,服务器客户端通信demo

    OSI模型分层 OSI模型是指国际标准化组织(ISO)提出的开放系统互连参考模型(Open System Interconnection Reference Model,OSI/RM),它将网络分为七 ...

  3. 是时候了解Java Socket底层实现了

    在Java中,提供了一系列Socket API,可以轻松建立两个主机之间的连接.读取数据,那底层到底怎么实现,很少人去关心.这其实最终还是通过调用操作系统提供得Socket接口完成(TCP/IP是由操 ...

  4. 基于TCP/IP协议的C++网络编程(API函数版)

    源代码:http://download.csdn.net/detail/nuptboyzhb/4169959 基于TCP/IP协议的网络编程 定义变量——获得WINSOCK版本——加载WINSOCK库 ...

  5. Java Socket底层实现浅析

    最近在学Java的socket编程,发现Java可以很简单的通过socketAPI实现网络通信,但是我一直有个疑问,Java的socket的底层是怎么实现的? 如果没记错的话Java的底层是C和C++ ...

  6. 轨迹系列——Socket总结及实现基于TCP或UDP的809协议方法

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.背景 在上一篇博客中我详细介绍了809协议的内容.809协议规范了通 ...

  7. -1-7 java 网络编程基本知识点 计算机网络 TCP/IP协议栈 通信必备 tcp udp

    计算机网络 是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来, 在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统. 网络编程 ...

  8. 轨迹系列7——Socket总结及实现基于TCP或UDP的809协议方法

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.背景 在上一篇博客中我详细介绍了809协议的内容.809协议规范了通 ...

  9. java android 读写三菱PLC 使用TCP/IP 协议

    本文将使用一个Github开源的组件库技术来读写三菱PLC和西门子plc数据,使用的是基于以太网的TCP/IP实现,不需要额外的组件,读取操作只要放到后台线程就不会卡死线程,本组件支持超级方便的高性能 ...

随机推荐

  1. Bower管理依赖库初体验

    比如一开始我用了jquery-1.10.2.min.js,后来要用bootstrap,但bootstrap依赖的确实2.0.3版本的jquery,那又要下载一个去替换原来的,这样的事情发生多了就会觉得 ...

  2. nginx bind() to 0.0.0.0:**** failed (13: Permission denied)

    nginx 启动失败,日志里面报错信息如下: Starting nginx: nginx: [emerg] bind() to 0.0.0.0:**** failed (13: Permission ...

  3. hibernate反向工程 (eclipse和myeclipse)【转】

    myeclipse下hibernate反向工程: 1.选择myeclipse hibernate视图 2.建立与后台数据库的连接 1).configure database driver: 2).添加 ...

  4. python安装包是出现错误解决

    /usr/lib/gcc/x86_64-linux-gnu/4.8/include-fixed/limits.h:168:61: fatal error: limits.h: No such file ...

  5. iptables不小心把127.0.0.1封了,导致redis连不上

    写了个脚本扫描apache日志,自动把恶意攻击者的ip交给iptables给封掉 谁知道一不小心把127.0.0.1也给封了... 直接导致redis无法链接. redis-server服务正常启动, ...

  6. linux中升级jdk的方法

    先查看java的安装情况: # rpm -qa|grep java 卸载: rpm -e --nodeps ******* 然后再安装就可以了

  7. Spring之替换Bean的返回结果,替换Bean的方法实例

    Spring是一个非常强悍的框架+容器,其中有代理模式(动态代理模式)的极致体现.下面是两个比较让人感觉精彩的代码使用,重点关注main方法中的ClassPathXMlApplicationConte ...

  8. IO/序列化/JSON

    一.读写文件 1.open:打开文件 open(path, mode, encoding='xxx', errors='ignore') mode取值:rU 或 Ua 以读方式打开, 同时提供通用换行 ...

  9. Unity3D优化之合并网格

    原文地址点击这里

  10. python安装libxml2和pyquery

    安装.net framework 4.5.2 https://www.microsoft.com/zh-CN/download/details.aspx?id=42641 安装C编译器 python2 ...