在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,