前面两篇分析了TCP和UDP协议,本篇来分析一下Socket,有了前面的基础,对理解Socket有很大的帮助,同时也是对TCP和UDP的更深层次的了解,经过多天的资料研究和代码分析,对socket也算是有了一个清楚的认识,鉴于网上的知识比较散,所以想把关于socket的知识整理一下,希望能够帮助想理解socket的童鞋,本着这些目的,本篇就来仔细分析一下Socket的原理。
文章内容有点长,先罗列一下需要分析的要点:
1. Socket是什么?
2. Java中Socket的使用
3. Java中Socket的源码分析
4. Linux系统中Socket对TCP的详细处理过程
1. Socket是什么?
我们知道进程通信的方法有管道、命名管道、信号、消息队列、共享内存、信号量,这些方法都要求通信的两个进程位于同一个主机。但是如果通信双方不在同一个主机又该如何进行通信呢?
在计算机网络中有一个tcp/ip协议族,使用tcp/ip协议族就能达到我们想要的效果,如下图所示:
socket所处层次
图中可以看到socket是位于应用层和传输层之间的一个抽象层,那么它存在的意义又是什么呢?其实没有socket抽象层,也是可以的,但是,当我们使用不同的协议进行通信时就得使用不同的接口,还得处理不同协议的各种细节,这就增加了开发的难度,软件也不易于扩展。于是UNIX BSD就发明了socket这种东西,socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用,通过系统调用我们可以方便的使用磁盘(文件操作),使用内存,而无需自己去进行磁盘读写,内存管理。socket其实也是一样的东西,就是提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能了。
2.Java中Socket的使用
在Java中,我们使用Socket有两种,一个是基于TCP的Socket,一个是基于UDP的DatagramSocket,本篇只分析基于TCP的Socket。现在我们来看Socket的使用:
客户端:指明主机端口创建Socket,获取输出流,写入数据。
Socket socket = null;
OutputStream os = null;
try {
socket = new Socket("192.168.1.106",9200);
os = socket.getOutputStream();
os.write("hello server !".getBytes());
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(os != null){
os.close();
}
if(socket != null){
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
服务端:指明绑定的端口,创建ServerSocket,开启端口监听。
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket(9200);
} catch (IOException e) {
return;
}
while (isRunning){
try {
Socket client = serverSocket.accept();
handleClient(client);
} catch (IOException e) {
e.printStackTrace();
}
}
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
这里只是一个简单的示例,当然实际开发中可能需要更多的东西,比如在handleClient(client)处理数据的时候加入线程池去处理,以及client的关闭等,这里只是简单的使用,主要目的还是看其源码。
3.Java中Socket的源码分析
3.1 服务端ServerSocket的源码解析
为了便于理解,我们先分析服务端ServerSocket的源码。通常我们在创建的时候需要指明绑定的端口,来看其构造函数:
public ServerSocket(int port) throws IOException {
this(port, 50, null);
}
public ServerSocket(int port, int backlog) throws IOException {
this(port, backlog, null);
}
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
setImpl();
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException(
"Port value out of range: " + port);
if (backlog < 1)
backlog = 50;
try {
bind(new InetSocketAddress(bindAddr, port), backlog);
} catch(SecurityException e) {
close();
throw e;
} catch(IOException e) {
close();
throw e;
}
}
ServerSocket有三个指明端口的构造函数,其中参数backlog表示已建立连接的最大数量,也就是accept中未被我们取走的最大连接数,这个现在不理解也没关系,后面会重点介绍这个参数,这里当未指明时,它的值默认是50;InetAddress是对IP、DNS、端口等信息的封装类。
我们看到,在构造函数中,先调用了setImpl()方法,看其源码:
private void setImpl() {
if (factory != null) {
impl = factory.createSocketImpl();
checkOldImpl();
} else {
// No need to do a checkOldImpl() here, we know it's an up to date
// SocketImpl!
impl = new SocksSocketImpl();
}
if (impl != null)
impl.setServerSocket(this);
}
factory是SocketImplFactory类型,是创建SocketImpl的工厂类;impl是SocketImpl类型。
/**
* The factory for all server sockets.
*/
private static SocketImplFactory factory = null;
/**
* The implementation of this Socket.
*/
private SocketImpl impl;
当我们调用构造函数的时候factory肯定是空的,所以此时会给ServerSocket的impl变量赋值为SocksSocketImpl的对象,然后调用 impl.setServerSocket(this)将ServerSocket自己和SocksSocketImpl关联起来。
回到构造函数,接下来会判断端口的正确性以及backlog的处理,最后调用了bind(new InetSocketAddress(bindAddr, port), backlog)方法来绑定该端口,看其源码:
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;
}
}
首先进行一大堆的判断,之后获取SecurityManager,这是一个安全策略管理器,在执行一些不安全、敏感等操作之前通常需要用到这个东西来先检查一下,比如这里在监听之前,调用了checkListen()方法,来检查当前线程是否允许在指定端口的连接请求过程中的使用wait等待,如果不允许,则会抛出异常。接着继续往下看,getImpl()源码:
SocketImpl getImpl() throws SocketException {
if (!created)
createImpl();
return impl;
}
void createImpl() throws SocketException {
if (impl == null)
setImpl();
try {
impl.create(true);
created = true;
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}
/**
* Creates a socket with a boolean that specifies whether this
* is a stream socket (true) or an unconnected UDP socket (false).
*/
protected synchronized void create(boolean stream) throws IOException {
this.stream = stream;
if (!stream) {
ResourceManager.beforeUdpCreate();
// only create the fd after we know we will be able to create the socket
fd = new FileDescriptor();
try {
socketCreate(false);
} catch (IOException ioe) {
ResourceManager.afterUdpClose();
fd = null;
throw ioe;
}
} else {