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

在这里不讨论TCP的三次握手四次挥手等,只讨论一下操作系统提供的接口,以及这些接口的使用,还有Java Socket底层是如何做的。

首先了解一下操作系统为我们提供的Socket编程接口。

拿Windows举例,提供了socket、bind、listen、accept、connect、send、recv等函数,如果了解过Socket编程的小伙伴应该一眼能看出这些函数是干什么得,如connect进行连接,send发送数据,recv接收数据。(鄙人有幸四年前研究过一些,后来转Java就在没怎么深入,但是现在看起来还倍感亲切)。。。

(并且在Linux中也同样有这一系列函数)

然后我们试着用这些函数去创建一个简单得通信程序,为了简单、易懂,将使用VB进行编程。(或者说,重新回忆一下当初的入门语言),并且,懂其他语言的人也能轻松把VB语言翻译成自己拿手的。

首先需要声明用到得一些函数,如下,这是非常痛苦的一点。

Private Type SOCKADDR
sin_family As Integer
sin_port As Integer
sin_addr As Long
sin_zero As String * 8
End Type Private Declare Function socket Lib "ws2_32.dll" (ByVal af As Long, ByVal lType As Long, ByVal protocol As Long) As Long
Private Declare Function bind Lib "ws2_32.dll" (ByVal s As Long, ByRef addr As SOCKADDR, ByVal namelen As Long) As Long
Private Declare Function listen Lib "ws2_32.dll" (ByVal s As Long, ByVal backlog As Long) As Long
Private Declare Function recv Lib "ws2_32.dll" (ByVal s As Long, ByVal buf As String, ByVal lLen As Long, ByVal flags As Long) As Long
Private Declare Function accept Lib "ws2_32.dll" (ByVal s As Long, ByRef addr As SOCKADDR, ByRef addrlen As Long) As Long
Private Declare Function send Lib "ws2_32.dll" (ByVal s As Long, ByVal buf As String, ByVal lLen As Long, ByVal flags As Long) As Long
Private Declare Function closesocket Lib "ws2_32.dll" (ByVal s As Long) As Long
Private Declare Function connect Lib "ws2_32.dll" (ByVal s As Long, ByRef name As SOCKADDR, ByVal namelen As Long) As Long Private Const WS2API_DECNET_MAX As Long = 10 Private Const sockaddr_size = 16
Private Const WSA_DESCRIPTIONLEN = 256
Private Const WSA_DescriptionSize = WSA_DESCRIPTIONLEN + 1
Private Const WSA_SYS_STATUS_LEN = 128
Private Const WSA_SysStatusSize = WSA_SYS_STATUS_LEN + 1
Private Declare Function WSAGetLastError Lib "ws2_32.dll" () As Long
Private Type WSAData
wVersion As Integer
wHighVersion As Integer
szDescription As String * WSA_DescriptionSize
szSystemStatus As String * WSA_SysStatusSize
iMaxSockets As Integer
iMaxUdpDg As Integer
lpVendorInfo As Long
End Type
Private Declare Function WSAStartup Lib "ws2_32.dll" (ByVal wVersionRequired As Integer, ByRef lpWsAdata As WSAData) As Long
Private Declare Function WSACleanup Lib "ws2_32.dll" () As Long Private Const AF_INET As Long = 2
Private Const SOCK_STREAM As Long = 1
Private Const IPPROTO_TCP As Long = 6
Private Declare Function inet_addr Lib "ws2_32.dll" (ByVal cp As String) As Long
Private Declare Function htons Lib "ws2_32.dll" (ByVal hostshort As Integer) As Integer Private Const SOMAXCONN As Long = &H7FFFFFFF
Private Const SOCKET_ERROR As Long = -1 Private Const AF_INET6 As Long = 23

接下来我们创建一个服务端Socket,接受客户端请求,并回显一段字符。在Java中服务端得编写流程应该很清楚把1.创建ServerSocket,2.调用ServerSocket得bind()进行绑定,3.不断accept()等待并返回客户端Socket。

用Windows Api实现大概也是这个过程。先上一张简单的流程图。

0.WSAStartup

在调用API进行套接字编程前,必须调用WSAStartup函数对Winsock服务的初始化,否则后续API调用都会失败。

参数一是指定加载的winsock版本号,高字节是次要版本,低字节是主版本,可以通过MAKEWORD(l,h)来指定。但是VB中没有这个,需要自己写一个。

Private Function MakeWord(ByVal bLow As Byte, ByVal bHigh As Byte) As Integer
MakeWord = bLow + bHigh * 256
End Function

lpWSAData:指向LPWSADATA结构的指针,该参数返回最终加载动态库的相关信息。

1.创建Socket

创建需要使用socket函数,它有三个参数,分别是:地址族或者协议族、socket类型、传输协议。

地址族:也就是IP地址类型,常用的有两种,AF_INET(IPv4)和AF_INET6(IPv6)。

socket类型:有SOCK_STREAM流格式套接字(面相连接)和SOCK_DGRAM数据报套接字(无连接),

传输协议:常用得有IPPROTO_TCP(TCP传输)和IPPROTO_UDP(UDP传输),如果为0,则根据上面设置的socket类型自动选择。



所以,创建一个面向连接的Socket代码如下,他的返回值也称之为套接字描述符。

Dim lpWsAdata As WSAData
WSAStartup(MakeWord(4, 4), lpWsAdata)
hSocket = socket(AF_INET, SOCK_STREAM, 0)

2.绑定

绑定也需要三个参数,分别为套接字描述符、sockaddr、sockaddr大小。第一个参数就不说了,是socket函数的返回值,第二个sockaddr是一个结构体,要绑定的信息在里面,赋值得时候需要用到htons函数和inet_addr进行转换(或者其他函数),如果端口直接写,则会失败。第三个参数可以通过len(vb)、sizeof(c)函数来获取。

 Dim lSocketAddress As SOCKADDR, hBind As Long
lSocketAddress.sin_family = AF_INET
lSocketAddress.sin_port = htons(2002)
lSocketAddress.sin_addr = inet_addr("127.0.0.1")
hBind = bind(hSocket, lSocketAddress, Len(lSocketAddress))

3.监听

listen函数让一个套接字处于监听连接请求的状态,调用它后,可以通过netstat 命令查看状态。如果不调用,后续accept会发生错误而导致直接返回。



参数有两个,分别代表套接字描述符,还是socket得返回值。第二个表示连接请求队列的最大长度,如果不断有新的请求进来,它们会按照先后顺序依次排队,直到这个队列满了,可以设置为SOMAXCONN,由系统来决定请求队列长度。

在通俗的说,假如服务端的队列此时大小是10,如果有10个人向服务端发起请求,而服务端暂时都没有调用accept。这时候在有其他的客户请求则会抛出异常,直到服务端调用accept从这个列队中取出一个,给后续腾出空间。



代码为:

 listen(hSocket, SOMAXCONN)

这个再Java中可能不需要手动调用,但是当我们调用serverSocket.bind()绑定时候,他紧接着就会调用listen方法,如果不指定backlog,则默认是50。

4.同意请求并返回数据

在java中accept()方法是阻塞的,直到有连接过来,同样accept函数也是阻塞式的,也就是在队列中没有连接时线程阻塞。accept也有三个参数,分别是socket描述符、第二个也就是存放连接请求的客户端地址,同样也是sockaddr结构体,第三个则是第二个参数大小。返回值是请求者的socket描述符。

send函数用来向socket中发送数据。参数分别是socket描述符、要发送数据得缓冲区、要发送数据的大小,最后一个一般为0。

  Dim lpAddR As SOCKADDR, hClientSocket As Long
hClientSocket = accept(hSocket, lpAddR, LenB(lpAddR)) Dim mBufData As String
mBufData = "Hello Window Socket" + vbCrLf
send hClientSocket, mBufData, LenB(mBufData), 0

上面得服务端基本就完成了,下面是客户端。

客户端的逻辑主要使用connect连接,并使用recv 进行接收数据,connect和服务端bind参数一样,就不说了,recv 的参数分别是socket描述符、接收数据存放得缓冲区、缓存区大小、最后一个一般也为0,recv成功时,返回值是接收数据的长度。

Dim lpWsAdata As WSAData

WSAStartup(MakeWord(4, 4), lpWsAdata)
Dim hSocket As Long
hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
If hSocket = 0 Then
MsgBox WSAGetLastError
Else
Dim lSocketAddress As SOCKADDR, mSocketConnectResult As Long
lSocketAddress.sin_family = AF_INET
lSocketAddress.sin_port = htons(2002)
lSocketAddress.sin_addr = inet_addr("127.0.0.1")
mSocketConnectResult = connect(hSocket, lSocketAddress, LenB(lSocketAddress))
If mSocketConnectResult = 0 Then
Dim sBuff As String * 255
recv hSocket, sBuff, Len(sBuff), 0
MsgBox sBuff
Else
MsgBox "连接错误"
End If
End If

总体各个函数得参数都比较简单,下面运行一下。

服务端启动后再启动客户端,客户端会弹出接收到服务端发送的数据。



如果要把上述客户端转换为Java代码,也很简单。

  public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 2002);
BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("数据:"+bufferedReader.readLine());
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}

当运行之后,效果如下

Java Socket 分析

当我们分析java底层到底调用什么方法时,往往发现都是些native方法,所以,我们需要一个openjdk源码,可以到http://hg.openjdk.java.net/下载。

在此之前,非常有必要清楚这张继承结构图。



从Java new一个Socket开始分析,看看底层做了哪些。

在空构造方法中直接调用了setImpl(),其中factory 只有在调用setSocketImplFactory后才会被赋值,所以先不管它,最重要要看SocksSocketImpl中做了哪些工作。

 void setImpl() {
if (factory != null) {
impl = factory.createSocketImpl();
checkOldImpl();
} else {
impl = new SocksSocketImpl();
}
if (impl != null)
impl.setSocket(this);
}

SocksSocketImpl构造方法中什么都没做,但是,这是表面得,别忘了我们类得初始化顺序,所以,我们需要看他得父类做了什么。

  SocksSocketImpl() {
// Nothing needed
}

在父类PlainSocketImpl中得静态代码快中判断了java运行环境版本和preferIPv4Stack,在官网对preferIPv4Stack的解释是:如果操作系统上有IPv6可用,则默认情况下,基础本机套接字将是一个IPv6套接字,该套接字使应用程序可以连接到IPv4和IPv6主机并接受来自它们的连接。但是,如果应用程序宁愿使用仅IPv4套接字,则可以将此属性设置为true。这意味着应用程序将无法与仅IPv6主机进行通信。他得默认值是false。



由静态代码块可得处useDualStackImpl值,我这里是true,则PlainSocketImpl中的impl实现类是DualStackPlainSocketImpl。

 PlainSocketImpl() {
if (useDualStackImpl) {
impl = new DualStackPlainSocketImpl(exclusiveBind);
} else {
impl = new TwoStacksPlainSocketImpl(exclusiveBind);
}
}

此时空构造方法大概就结束了,这似乎没做什么,当然,连接得逻辑在socket.connect()中。

省去前面的一些判断逻辑直接看重点。

public void connect(SocketAddress endpoint, int timeout) throws IOException {
.....
if (!created)
createImpl(true);
if (!oldImpl)
impl.connect(epoint, timeout);
else if (timeout == 0) {
if (epoint.isUnresolved())
impl.connect(addr.getHostName(), port);
else
impl.connect(addr, port);
} else
throw new UnsupportedOperationException("SocketImpl.connect(addr, timeout)"
connected = true;
}

上面主要通过 impl.connect(addr.getHostName(), port);这句去连接,impl是哪个类的实例在构造方法中已经初始化了,是SocksSocketImpl类,但是我们不能忽略createImpl(true);这句,他用来创建一个Socket,这其中关键点在于 impl.create(stream),但是SocksSocketImpl没有重写create方法,所以我们要到他父类里面找。

 void createImpl(boolean stream) throws SocketException {
if (impl == null)
setImpl();
try {
impl.create(stream);
created = true;
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}

他的父类PlainSocketImpl中又调用了 impl.create,而在上面我们已经知道impl实现类是谁了,以及如何决定。可DualStackPlainSocketImpl中也没有重写create方法,还需要往上走。

 protected synchronized void create(boolean stream) throws IOException {
impl.create(stream);
// set fd to delegate's fd to be compatible with older releases
this.fd = impl.fd;
}

所以到了AbstractPlainSocketImpl,他默认是流式的方式,关键点还是socketCreate,一个抽象方法,又必须交给之类实现

 protected synchronized void create(boolean stream) throws IOException {
this.stream = stream;
if (!stream) {
..........
} else {
fd = new FileDescriptor();
socketCreate(true);
}
..........
}
abstract void socketCreate(boolean isServer) throws IOException;

于是我们要回到DualStackPlainSocketImpl中查看socketCreate,这里就是尽头了,其中关键点在于socket0,是一个本地方法。

  void socketCreate(boolean stream) throws IOException {
if (fd == null)
throw new SocketException("Socket closed");
int newfd = socket0(stream, false /*v6 Only*/);
fdAccess.set(fd, newfd);
}
static native int socket0(boolean stream, boolean v6Only) throws IOException;

那就开始看socket0中做了什么。它在DualStackPlainSocketImpl.c中实现。

从这里我们发现了几个关键点,AF_INET6、SOCK_STREAM、SOCK_DGRAM,这非常像我们开头创建socket的时候指定的参数。但是他调用了NET_Socket,所以,我们还要继续看NET_Socket方法又干了什么。

JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
(JNIEnv *env, jclass clazz, jboolean stream, jboolean v6Only /*unused*/) {
int fd, rv, opt=0;
//创建Socket
fd = NET_Socket(AF_INET6, (stream ? SOCK_STREAM : SOCK_DGRAM), 0);
。。。此处省略一些
return fd;
}

我们通过跟踪,发现最终实现在net_util_md.c中,天哪,这不就是开头说的socket函数吗?并且他的参数和我们创建的方式一样。

socket已经算是底层了,在底层就是操作系统对socket的实现。

int NET_Socket (int domain, int type, int protocol) {
SOCKET sock;
sock = socket (domain, type, protocol);
if (sock != INVALID_SOCKET) {
SetHandleInformation((HANDLE)(uintptr_t)sock, HANDLE_FLAG_INHERIT, FALSE);
}
return (int)sock;
}

这不行啊,光看到了socket函数还不够,connect、listen等呢?

慢慢来,connect直接在DualStackPlainSocketImpl.c就能看到,也是由DualStackPlainSocketImpl中的socketConnect方法中调用,第一个参数就是socket的描述符。底层调用NET_InetAddressToSockaddr把InetAddress转换成SOCKETADDRESS,但是SOCKETADDRESS可不是windows提供的,是它自己定义的,这个结构中就包含了我们熟悉的sockaddr。

typedef union {
struct sockaddr sa;
struct sockaddr_in sa4;
struct sockaddr_in6 sa6;
} SOCKETADDRESS;
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_connect0
(JNIEnv *env, jclass clazz, jint fd, jobject iaObj, jint port) {
SOCKETADDRESS sa;
int rv, sa_len = 0;
if (NET_InetAddressToSockaddr(env, iaObj, port, &sa,
&sa_len, JNI_TRUE) != 0) {
return -1;
}
rv = connect(fd, &sa.sa, sa_len);
if (rv == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err == WSAEWOULDBLOCK) {
return java_net_DualStackPlainSocketImpl_WOULDBLOCK;
} else if (err == WSAEADDRNOTAVAIL) {
JNU_ThrowByName(env, JNU_JAVANETPKG "ConnectException",
"connect: Address is invalid on local machine, or port is not valid o
} else {
NET_ThrowNew(env, err, "connect");
}
return -1; // return value not important.
}
return rv;
}

再看一下listen,同样在看到DualStackPlainSocketImpl.c中,又是熟悉的身影,熟悉的参数,但是listen0只有服务端Socket才会调用,也就是ServerSocket。

JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_listen0
(JNIEnv *env, jclass clazz, jint fd, jint backlog) {
if (listen(fd, backlog) == SOCKET_ERROR) {
NET_ThrowNew(env, WSAGetLastError(), "listen failed");
}
}

recv函数在SocketInputStream.c中。所以说,socket.getInputStream()返回值就是SocketInputStream。

这里面做了非常多的工作,但是我们还是要看recv。还是熟悉的身影,熟悉的参数,熟悉的0。

JNIEXPORT jint JNICALL
Java_java_net_SocketInputStream_socketRead0(JNIEnv *env, jobject this,
jobject fdObj, jbyteArray data,
jint off, jint len, jint timeout)
{
。。。。。
nread = recv(fd, bufP, len, 0);
。。。。。
}

剩下的就不说了,如果调用Socket的有参构造方法,流程还是差不多的。这个就需要自己debug跟踪代码。

以上是对于Window,Linux下可能不同,下面是Linux的继承结构图,主要实现在PlainSocketImpl的一系列本地方法中。对应的c文件也就是PlainSocketImpl.c,



是时候了解Java Socket底层实现了的更多相关文章

  1. JAVA Socket 底层是怎样基于TCP/IP 实现的???

    首先必须明确:TCP/IP模型中有四层结构:       应用层(Application Layer).传输层(Transport  Layer).网络层(Internet Layer  ).链路层( ...

  2. Java Socket底层实现浅析

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

  3. Java Socket入门

    Java Socket底层采用TCP/IP协议通信,通信细节被封装,我们仅仅需要指定IP.端口,便能轻易地创建TCP或UDP连接,进行网络通信.数据的读写,可以使用我们熟悉的stream进行操作. T ...

  4. 基于JAVA Socket的底层原理分析及工具实现

    前言 在工作开始之前,我们先来了解一下Socket 所谓Socket,又被称作套接字,它是一个抽象层,简单来说就是存在于不同平台(os)的公共接口.学过网络的同学可以把它理解为基于传输TCP/IP协议 ...

  5. JAVA通信系列一:Java Socket技术总结

    本文是学习java Socket整理的资料,供参考. 1       Socket通信原理 1.1     ISO七层模型 1.2     TCP/IP五层模型 应用层相当于OSI中的会话层,表示层, ...

  6. Java Socket Server的演进 (一)

    最近在看一些网络服务器的设计, 本文就从起源的角度介绍一下现代网络服务器处理并发连接的思路, 例子就用java提供的API. 1.单线程同步阻塞式服务器及操作系统API 此种是最简单的socket服务 ...

  7. JAVA Socket超时浅析

    JAVA Socket超时浅析 套接字或插座(socket)是一种软件形式的抽象,用于表达两台机器间一个连接的"终端".针对一个特定的连接,每台机器上都有一个"套接字&q ...

  8. Java Socket(2): 异常处理

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

  9. [ 转载]JAVA Socket超时浅析

    JAVA Socket超时浅析 转载自 http://blog.csdn.net/sureyonder/article/details/5633647 套接字或插座(socket)是一种软件形 式的抽 ...

随机推荐

  1. js怎么获取动态链式属性呢?

    动态链式属性?我自己起的名字,样子就是 var data={ list:{ name:'zxf' } }var prop="list.name";console.log(data[ ...

  2. Windows server 2008 r2下安装sqlserver2012

    在微软官网上下载sqlserver2012镜像文件:用Alcohol 120%软件进行驱动安装. 如果C盘的容量不够的话,上边的路径可以修改

  3. python字符串复制的几种方法

    >>> list1 = [1,2] >>> id(list1) 50081032 >>> list2 = list1.copy() >> ...

  4. xshell 常用命令1

    date命令 date命令是显示或设置系统时间与日期. 很多shell脚本里面需要打印不同格式的时间或日期,以及要根据时间和日期执行操作.延时通常用于脚本执行过程中提供一段等待的时间.日期可以以多种格 ...

  5. 接口自动化测试平台 http://120.79.232.23

    接口自动化测试平台 http://120.79.232.23 T Name Latest commit message Commit time .idea 修改自动化用例修改接口时,其他接口信息被删的 ...

  6. Linux内核初探 之 进程(三) —— 进程调度算法

    一.基本概念 抢占 Linux提供抢占式多任务,基于时间片和优先级对进程进行强制挂起 非抢占的系统需要进程自己让步(yielding) 进程类型 IO消耗型 经常处于可运行态,等待IO操作过程会阻塞 ...

  7. hihoCoder 1128 二分查找

    Description Input and Output Codes 描述#1128 : 二分·二分查找 Description Nettle最近在玩<艦これ>,因此Nettle收集了很多 ...

  8. webpack的基本配置(初识)

    webpack能根据模块的依赖关系递归地构建一个依赖关系图,当中包含了应用程序所需要的所有模块,最后打包成一个或多个bundle.它有四个核心概念entry.output .loader.plugin ...

  9. java ThreadPoolExecutor初探

    导读:线程池是开发中使用频率比较高的组件之一,但是又有多少人真正了解其内部机制呢. 关键词:线程池 前言 线程池是大家开发过程中使用频率比较高的组件之一,但是其内部原理又有多少人真正清楚呢.最近抽时间 ...

  10. 前端开发: webStorm手机真机调试

    目录 1. 做好准备工作 2. 开始设置 3. 配置路径 4. 匹配路径设置 5. 设置完成,即可体验 文章首发微信公众号飙码人生 2018-05-21 18:21:15 1. 做好准备工作 安装we ...