socket通信总结
-
本文概述
整理了一下socket的基础用法,以及从源码层面理解HttpURLConnection的底层也是使用的socket机制进行的网络通信,socket的网络通信体现在应用层就是read和write操作。
-
Socket是什么
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“<font color="red">打开open –> 读写write/read –> 关闭close</font>”模式来操作。
套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
Server端Listen监听某个端口是否有连接请求,Client端向Server 端发出连接请求,Server端向Client端发回Accept接受消息。这样一个连接就建立起来了。Server端和Client端都可以通过Send,Write等方法与对方通信。
对于一个功能齐全的Socket,都要包含以下基本结构,其工作过程包含以下四个基本的步骤:
1.创建Socket;
2.打开连接到Socket的输入/出流;
3.按照一定的协议对Socket进行读/写操作;
4.关闭Socket。
-
Java API
-
基于TCP的socket实现
public class SocketClient { public static void main(String[] args) throws InterruptedException { try { // 和服务器创建连接 Socket socket = new Socket("localhost",8088); // 要发送给服务器的信息 OutputStream os = socket.getOutputStream(); PrintWriter pw = new PrintWriter(os); pw.write("客户端发送信息"); pw.flush(); socket.shutdownOutput(); // 从服务器接收的信息 InputStream is = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String info = null; while((info = br.readLine())!=null){ System.out.println("我是客户端,服务器返回信息:"+info); } br.close(); is.close(); os.close(); pw.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }
public class SocketServer { public static void main(String[] args) { try { // 创建服务端socket ServerSocket serverSocket = new ServerSocket(8088); // 创建客户端socket Socket socket = new Socket(); //循环监听等待客户端的连接 while(true){ // 监听客户端 socket = serverSocket.accept(); ServerThread thread = new ServerThread(socket); thread.start(); InetAddress address=socket.getInetAddress(); System.out.println("当前客户端的IP:"+address.getHostAddress()); } } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } }
public class ServerThread extends Thread{ private Socket socket = null; public ServerThread(Socket socket) { this.socket = socket; } @Override public void run() { InputStream is=null; InputStreamReader isr=null; BufferedReader br=null; OutputStream os=null; PrintWriter pw=null; try { is = socket.getInputStream(); isr = new InputStreamReader(is); br = new BufferedReader(isr); String info = null; while((info=br.readLine())!=null){ System.out.println("我是服务器,客户端说:"+info); } socket.shutdownInput(); os = socket.getOutputStream(); pw = new PrintWriter(os); pw.write("服务器欢迎你"); pw.flush(); } catch (Exception e) { // TODO: handle exception } finally{ //关闭资源 try { if(pw!=null) pw.close(); if(os!=null) os.close(); if(br!=null) br.close(); if(isr!=null) isr.close(); if(is!=null) is.close(); if(socket!=null) socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
-
基于UDP的socket实现
public class SocketClient { public static void main(String[] args) { try { // 要发送的消息 String sendMsg = "客户端发送的消息"; // 获取服务器的地址 InetAddress addr = InetAddress.getByName("localhost"); // 创建packet包对象,封装要发送的包数据和服务器地址和端口号 DatagramPacket packet = new DatagramPacket(sendMsg.getBytes(), sendMsg.getBytes().length, addr, 8088); // 创建Socket对象 DatagramSocket socket = new DatagramSocket(); // 发送消息到服务器 socket.send(packet); // 关闭socket() socket.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
public class SocketServer { public static void main(String[] args) { try { // 要接收的报文 byte[] bytes = new byte[1024]; DatagramPacket packet = new DatagramPacket(bytes, bytes.length); // 创建socket并指定端口 DatagramSocket socket = new DatagramSocket(8088); // 接收socket客户端发送的数据。如果未收到会一致阻塞 socket.receive(packet); String receiveMsg = new String(packet.getData(),0,packet.getLength()); System.out.println(packet.getLength()); System.out.println(receiveMsg); // 关闭socket socket.close(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } }
-
可以看到,对于TCP方式,getOutputStream()就是建立连接的过程,server端一直循环接收socket连接请求,write就是发送数据的过程,server端新线程循环读取发送过来的数据。对于UDP方式,因为不是面向连接的,所以client端只需要知道往哪发送数据就好,不需要getOutputStream,直接send发送数据报,server端调用receive方法接收该应用程序(8088)发送过来的数据。
-
HttpURLConnection的调用
在之前分析HttpURLConnection的调用过程时,最终走到了Connection类的connect方法:
public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest) throws IOException { if (connected) throw new IllegalStateException("already connected"); socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket(); Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout); socket.setSoTimeout(readTimeout); in = socket.getInputStream(); out = socket.getOutputStream(); if (route.address.sslSocketFactory != null) { upgradeToTls(tunnelRequest); } else { initSourceAndSink(); httpConnection = new HttpConnection(pool, this, source, sink); } connected = true; }
Platform.get().connectSocket方法调用了socket的connect方法:
public void connectSocket(Socket socket, InetSocketAddress address, int connectTimeout) throws IOException { socket.connect(address, connectTimeout); }
socket的connect方法里关键部分如下:
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方法,impl是什么,从createImpl中最终可知是SocksSocketImpl:
void createImpl(boolean stream) throws SocketException { if (impl == null) setImpl(); try { impl.create(stream); created = true; } catch (IOException e) { throw new SocketException(e.getMessage()); } } 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.setSocket(this); }
SocksSocketImpl中的connect方法的关键部分如下:
if(server == null){ super.connect(epoint, remainingMillis(deadlineMillis)); return; } else { // Connects to the SOCKS server try { privilegedConnect(server, serverPort, remainingMillis(deadlineMillis)); } catch (IOException e) { throw new SocketException(e.getMessage()); } } // cmdIn & cmdOut were initialized during the privilegedConnect() call BufferedOutputStream out = new BufferedOutputStream(cmdOut, 512); InputStream in = cmdIn;
从“Connects to the SOCKS server”这句官方注释上也能看出来,privilegedConnect就是连接server的地方:
private synchronized void privilegedConnect(final String host, final int port, final int timeout) throws IOException { try { AccessController.doPrivileged( new java.security.PrivilegedExceptionAction<Void>() { public Void run() throws IOException { superConnectServer(host, port, timeout); cmdIn = getInputStream(); cmdOut = getOutputStream(); return null; } }); } catch (java.security.PrivilegedActionException pae) { throw (IOException) pae.getException(); } }
这里的getInputStream()和getOutputStream()是其父类的父类AbstractPlainSocketImpl的方法:
protected synchronized InputStream getInputStream() throws IOException { synchronized (fdLock) { if (isClosedOrPending()) throw new IOException("Socket Closed"); if (shut_rd) throw new IOException("Socket input is shutdown"); if (socketInputStream == null) socketInputStream = new SocketInputStream(this); } return socketInputStream; }
所以最终返回的inputStream就是SocketInputStream对象。
下面的代码就是使用cmdIn和cmdOut进行write和read,这时候传输的数据都是连接建立后、应用数据传输前需要准备的一些基本参数。
这个连接过程完了之后再回到Connection的connect方法往下走,接下来就是直接调用socket的getOutputStream和getInputStream方法获取应用数据传输的输入输出流(应用数据的传输就是用这两个对象实现)。以inputStream为例,read操作就相当于调用了SocketInputStream的read操作,read方法里的关键方法就是socketRead方法,socketRead又调用了socketRead0方法:
/** * Reads into an array of bytes at the specified offset using * the received socket primitive. * @param fd the FileDescriptor * @param b the buffer into which the data is read * @param off the start offset of the data * @param len the maximum number of bytes read * @param timeout the read timeout in ms * @return the actual number of bytes read, -1 is * returned when the end of the stream is reached. * @exception IOException If an I/O error has occurred. */ private native int socketRead0(FileDescriptor fd, byte b[], int off, int len, int timeout) throws IOException;
这是个native方法,里面的就是网络协议的通信部分了。
HttpEngine的connect方法是在sendRequest中调用的,sendRequest是在HttpURLConnectionImpl的execute中调用的,sendRequest之后有一个readResponse方法:
public final void readResponse() throws IOException { if (response != null) return; if (responseSource == null) throw new IllegalStateException("call sendRequest() first!"); if (!responseSource.requiresConnection()) return; // Flush the request body if there's data outstanding. if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) { bufferedRequestBody.flush(); } if (sentRequestMillis == -1) { if (OkHeaders.contentLength(request) == -1 && requestBodyOut instanceof RetryableSink) { // We might not learn the Content-Length until the request body has been buffered. long contentLength = ((RetryableSink) requestBodyOut).contentLength(); request = request.newBuilder() .header("Content-Length", Long.toString(contentLength)) .build(); } transport.writeRequestHeaders(request); } if (requestBodyOut != null) { if (bufferedRequestBody != null) { // This also closes the wrapped requestBodyOut. bufferedRequestBody.close(); } else { requestBodyOut.close(); } if (requestBodyOut instanceof RetryableSink) { transport.writeRequestBody((RetryableSink) requestBodyOut); } } transport.flushRequest(); response = transport.readResponseHeaders() .request(request) .handshake(connection.getHandshake()) .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis)) .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis())) .setResponseSource(responseSource) .build(); connection.setHttpMinorVersion(response.httpMinorVersion()); receiveHeaders(response.headers()); if (responseSource == ResponseSource.CONDITIONAL_CACHE) { if (validatingResponse.validate(response)) { transport.emptyTransferStream(); releaseConnection(); response = combine(validatingResponse, response); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). OkResponseCache responseCache = client.getOkResponseCache(); responseCache.trackConditionalCacheHit(); responseCache.update(validatingResponse, cacheableResponse()); if (validatingResponse.body() != null) { initContentStream(validatingResponse.body().source()); } return; } else { closeQuietly(validatingResponse.body()); } } if (!hasResponseBody()) { // Don't call initContentStream() when the response doesn't have any content. responseTransferSource = transport.getTransferStream(cacheRequest); responseBody = responseTransferSource; return; } maybeCache(); initContentStream(transport.getTransferStream(cacheRequest)); }
在这里封装response。
所以HttpURLConnection最后也是调用了socket的getInputStream进行读取、调用getOutputStream进行写入。
-
总结
socket是通过SocksSocketImpl的getInputStream和getOutputStream方法来进行网络通信的,而HttpURLConnection底层也是通过socket方式来进行网络通信的。socket的网络通信体现在应用层就是read和write操作,他们实际上调用了对应的native方法进行网络通信。底层就是调用了JVM的网络通信,有时间再研究一下。