在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. maven依赖找不到,快速解决

    以微信支付依赖为例子 wxpay-sdk-3.0.9.jar1.阿里云仓库搜索地址https://maven.aliyun.com/mvn/search 2.搜索你要找的依赖,对号入座 3.确保mav ...

  2. mvn -v报java.lang.ClassNotFoundException

    Tips: 比如要下载版本3.2.5的,请选择binaries下的apache-maven-3.2.5-bin.zip. binaries 指的是可以执行的. source 指的源码. 下载地址:ht ...

  3. 【转载】[基础知识]【网络编程】TCP/IP

    转自http://mc.dfrobot.com.cn/forum.php?mod=viewthread&tid=27043 [基础知识][网络编程]TCP/IP iooops  胖友们楼主我又 ...

  4. 暑假集训第六周contest1

    51Nod - 1413 权势二进制 题意:就是讲给出一个数n,让你求最少由多少个像0,1,10,11......这样的二进制数相加构成:样例n=9就是由9个二进制1相加组成,我不懂比赛的时候我为什么 ...

  5. python中字符串操作--截取,查找,替换

    python中,对字符串的操作是最常见的,python对字符串操作有自己特殊的处理方式. 字符串的截取 python中对于字符串的索引是比较特别的,来感受一下: s = '123456789' #截取 ...

  6. swoole I/O 模型

    I/O即Input/Output,输入和输出的意思.在计算机的世界里,涉及到数据交换的地方,比如磁盘.网络等,就需要I/O接口. 通常,I/O是相对的.比如说你打开浏览器,通过网络I/O获取我们网站的 ...

  7. 服务治理与RPC · 跬步

    以前写过Django中使用zerorpc的方法,但是由于我们的Django是运行在gevent下,而zeromq需要启动一个后台进程处理消息,与gevent使用的greenlet携程是冲突的. 在Ja ...

  8. 当鼠标hover的时候,使用tip将overflow:hidden隐藏的文字显示完全

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  9. VUE实现Studio管理后台(二):Slot实现选项卡tab切换效果,可自由填装内容

    作为RXEditor的主界面,Studio UI要使用大量的选项卡TAB切换,我梦想的TAB切换是可以自由填充内容的.可惜自己不会实现,只好在网上搜索一下,就跟现在你做的一样,看看有没有好事者实现了类 ...

  10. 前端每日实战:4# 视频演示如何用纯 CSS 创作一个金属光泽 3D 按钮特效

    效果预览 按下右侧的"点击预览"按钮在当前页面预览,点击链接全屏预览. https://codepen.io/zhang-ou/full/MGeRRO 可交互视频教程 此视频是可以 ...