java多线程网络编程——探究java socket与linux socket

  在当今互联网时代,网络显得尤其重要,不管是QQ、微信,仍是网络游戏,都离不开网络通讯,而java做为当web开发最火的语言,相信你们都接触过java网络编程,那java网络通讯中调用了系统级的哪些接口呢?今天,我就带着你们共同探究java socket与linux socket之间的千丝万缕。java

  说到网络通讯怎么能不谈计算机网络呢,简而言之,网络界主要有两种网络分层模型:即OSI和TCP/IP,OSI有7层,TCP/IP则将网络分为4层,如今TCP/IP模型是事实上的网络标准,而咱们结合二者,通常都说TCP/IP 5层协议模型,下面给一张图来讲明:linux

  

 

  那socket接口在哪一层呢,事实上socket是系统为咱们提供的网络通讯接口,若是非要说它在哪一层的话,那么socket就位于应用层和传输层之间,经过socket接口对网络的抽象,屏蔽了下面各层那么多复杂的协议,给人感受好像是用socket套接字直接与对方通讯同样,这样大大简化了程序员的工做,使得程序员根本不须要关心底层的东西,只须要经过socket接口与应用层和传输层打交道便可,固然其实大部分时间程序员只须要关心应用层便可。通常来说,传输层有两大协议,即面向链接的TCP和无链接的UDP协议,所谓面向链接是指传输是有序的、无差错的,可能更费时,但颇有用;而无链接是指尽最大努力交付,出点差错也无所谓。程序员

  socket会用到运输层的服务,那么固然socket接口也有基于tcp的socket和基于udp的socket之分,udp比较简单,今天就以基于tcp的socket为例,使用java语言编写一个socket多线程网络聊天程序,探究java socket背后的工做原理。web

 

  在编写代码以前先简单介绍下java网络编程中最重要的socket接口:  编程

  一、Scoket又称“套接字”,其由IP地址和端口号组成,能够说它惟一标识了网络上的某个进程,应用程序一般经过“套接字”向网络发出请求或者应答网络请求;在 java中Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是创建网络链接时使用的,在链接成功时,应用程序两端都会产生一个Socket实例,操做这个实例,完成所需的会话。对于一个网络链接来讲,套接字是平等的,并无差异,不由于在服务器端或在客户端而产生不一样的级别,不论是Socket仍是ServerSocket他们的工做都是经过Socket类和其子类来完成的服务器

  二、创建Socket连接可分三个步骤:
         1.服务器监听
         2.客户端发出请求
         3.创建连接
         4.通讯
  三、Socket特色:
          1.基于TCP连接,数据传输有保障
          2.适用于创建长时间的连接,不像HTTP那样会随机关闭
          3.Socket编程应用于即时通信
微信

  通俗点讲,socket是一个电话号码,每一个手机都有一个电话号码,当你想打电话给对方时,首先要知道对方的电话号码即socket,对方的手机要保证有话费的,当播出号码的时候,对方手机响了,按下吉接听键,这是就创建了双方的链接,就能双方相互通话了。网络

  下面进入正题,开始写java多线程网络聊天程序,首先是服务器代码:多线程

package socket; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; /*** * 多线程TCP服务器,为每一个链接创立一个线程 * @author mjc * @version 1.1 2019-12-4 */ public class TCPServer { public static void main(String[] args){ try(ServerSocket s = new ServerSocket(8189)) { int i = 1; while (true){ Socket incoming = s.accept(); System.out.println("链接序号:"+i); Runnable r = new ServerThread(incoming); Thread t = new Thread(r); t.start(); i++; } } catch (IOException e) { e.printStackTrace(); } } } class ServerThread implements Runnable{ private Socket incoming; public ServerThread(Socket incoming){ this.incoming = incoming; } public void run(){ try(InputStream inputStream = incoming.getInputStream(); OutputStream outputStream = incoming.getOutputStream()){ Scanner in = new Scanner(inputStream,"GBK"); PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true); //out.println("Hello! Enter BYE to exit."); boolean done = false; while (!done&&in.hasNextLine()){ String line = in.nextLine(); System.out.println("客户端发来: "+line); //out.println("Echo: "+line); if(line.trim().equals("BYE")) { System.out.println("我发给客户端: BYE,BYE!"); System.out.println("与客户端链接断开"); out.println("BYE,BYE!"); done = true;} else System.out.println("我发给客户端: hi!"); out.println("hi!"); } } catch (IOException e) { e.printStackTrace(); } } }
  服务器主要设计了两个类,一个是TCPServer,一个是实现了Runnable接口的线程类ServerThread,用来实现多线程,在主方法中,首先用ServerSocket s = new ServerSocket(8189)
建立一个端口为8189的监听端口,而后循环使用Socket incoming = s.accept();接受客户端的链接并创建相应的socket,每创建一个链接便启动一个服务器线程,这样即可以和多个客户端进行通讯。
服务器主要是收到客户端的字符串,并回送一个hi,直到客户端发出BYE,服务器便向对方回送BYE,BYE!,而后断开与客户端的链接。
  接下来编写两个客户端程序,将同时与服务器进行通讯,客户端1源码:
package socket; import java.io.*; import java.net.Socket; import java.net.UnknownHostException; import java.util.Scanner; public class TCPClient1 { public static void main(String[] args){ try (Socket s = new Socket("127.0.0.1",8189); InputStream inputStream = s.getInputStream(); OutputStream outputStream = s.getOutputStream()) { System.out.println("客户端1链接到服务器成功!"); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); Scanner in = new Scanner(inputStream,"GBK"); PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true); System.out.println("开始与服务器聊天,说BYE去结束聊天."); boolean done =false; while(!done){ String line = br.readLine(); if(line.equals("BYE")) done = true; out.println(line); System.out.println("服务器说: "+in.nextLine()); } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }

  客户端2源码:dom

package socket; import java.io.*; import java.net.Socket; import java.net.UnknownHostException; import java.util.Scanner; public class TCPClient2 { public static void main(String[] args){ try (Socket s = new Socket("127.0.0.1",8189); InputStream inputStream = s.getInputStream(); OutputStream outputStream = s.getOutputStream()) { System.out.println("客户端2链接到服务器成功!"); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); Scanner in = new Scanner(inputStream,"GBK"); PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true); System.out.println("开始与服务器聊天,说BYE去结束聊天."); boolean done =false; while(!done){ String line = br.readLine();
          System.out.println("客户端发来: "+line);
if(line.equals("BYE"))
            System.out.println("我发给客户端: BYE,BYE!"); done
= true; out.println(line); System.out.println("服务器说: "+in.nextLine()); } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }

  客户端首先使用 Socket s = new Socket("127.0.0.1",8189);创建了一个对方ip为127.0.0.1(即本地主机),端口为8189的socket,客户端会向这个socket发出创建请求,若是创建成功则返回一个socket s,用户可在命令行敲出字符串,这个消息会发送到指定地址的服务器进程,当客户端输入BYE的时候,服务器会回送一个BYE,BYE!而后断开与链接。

  如今让它们跑起来试试,先开启服务端,而后开启两个客户端:

  如今在客户端输入BYE试试:

 

 

  能够看到,用java来写网络通讯程序仍是比较简单的,服务端只用到了 ServerSocket类及其accept()方法和socket类,客户端也就用到了socket类,这样二者便能通畅的对话了,java语言为咱们提供的网络编程API让咱们没必要关心底层的细节,然而其实它的通讯也是利用了系统的socket API,在探究java的socket以前咱们先来看看linux 为咱们提供的socket API:

  这里再提一次,socket就是抽象封装了传输层如下软硬件行为,为上层应用程序提供进程/线程间通讯管道。就是让应用开发人员不用管信息传输的过程,直接用socket API就OK了。贴个TCP的socket示意图体会一下:

  

 

   如今以TCP client/server模型为例子看一下linux socket通讯的整个过程:

socket API函数以下:

socket: establish socket interface
gethostname: obtain hostname of system
gethostbyname: returns a structure of type hostent for the given host name
bind: bind a name to a socket
listen: listen for connections on a socket
accept: accept a connection on a socket
connect: initiate a connection on a socket
setsockopt: set a particular socket option for the specified socket.
close: close a file descriptor
shutdown: shut down part of a full-duplex connection

1. socket()

#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); - 参数说明 domain: 设定socket双方通讯协议域,是本地/internet ip4 or ip6 Name Purpose Man page AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) type: 设定socket的类型,经常使用的有 SOCK_STREAM - 通常对应TCP、sctp SOCK_DGRAM - 通常对应UDP SOCK_RAW - protocol: 设定通讯使用的传输层协议 经常使用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,能够设置为0,系统本身选定。注意protocol和type不是随意组合的。

 

  socket() API是在glibc中实现的,该函数又调用到了kernel的sys_socket(),调用链以下:

 

 

 

  详细的kernel实现我没有去读,大致上这样理解。调用socket()会在内核空间中分配内存而后保存相关的配置。同时会把这块kernel的内存与文件系统关联,之后即可以经过filehandle来访问修改这块配置或者read/write socket。操做socket就像操做file同样,应了那句unix一切皆file。提示系统的最大filehandle数是有限制的,/proc/sys/fs/file-max设置了最大可用filehandle数。

2. bind()

#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明 sockfd:以前socket()得到的file handle addr:绑定地址,可能为本机IP地址或本地文件路径 addrlen:地址长度 功能说明 bind()设置socket通讯的地址,若是为INADDR_ANY则表示server会监听本机上全部的interface,若是为127.0.0.1则表示监听本地的process通讯(外面的process也接不进啊)。

3. listen()

 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); 参数说明 sockfd:以前socket()得到的file handle backlog:设置server能够同时接收的最大连接数,server端会有个处理connection的queue,listen设置这个queue的长度。 功能说明 listen()只用于server端,设置接收queue的长度。若是queue满了,server端能够丢弃新到的connection或者回复客户端ECONNREFUSED。

4. accept()

 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 参数说明: addr:对端地址 addrlen:地址长度 功能说明: accept()从queue中拿出第一个pending的connection,新建一个socket并返回。 新建的socket咱们叫connected socket,区别于前面的listening socket。 connected socket用来server跟client的后续数据交互,listening socket继续waiting for new connection。 当queue里没有connection时,若是socket经过fcntl()设置为 O_NONBLOCK,accept()不会block,不然通常会block。

5. connect()

 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明: sockfd: socket的标示filehandle addr:server端地址 addrlen:地址长度 功能说明: connect()用于双方链接的创建。 对于TCP链接,connect()实际发起了TCP三次握手,connect成功返回后TCP链接就创建了。 对于UDP,因为UDP是无链接的,connect()能够用来指定要通讯的对端地址,后续发数据send()就不须要填地址了。 固然UDP也能够不使用connect(),socket()创建后,在sendto()中指定对端地址。

   以上就是系统为咱们提供的主要socket接口函数以及C/S模型使用TCP通讯的过程,这些函数都是用C语言实现的,java底层也是用C语言写的,

  如今让咱们来追踪java网络程序中调用的socket接口过程:

  上面的客户端只是实例化Socket类即可向对方创建链接,就先从Socket谈起吧,在Idea IDE中追踪Socket:

(1)起始、

Socket s = new Socket("127.0.0.1",8189)

(2)追踪Socket、

 public Socket(String host, int port) throws UnknownHostException, IOException { this(host != null ? new InetSocketAddress(host, port) : new InetSocketAddress(InetAddress.getByName((String)null), port), (SocketAddress)null, true); }

(3)发如今调用构造方法中,又调用了构造函数,跟踪这个this()构造函数:

private Socket(SocketAddress address, SocketAddress localAddr, boolean stream) throws IOException { this.created = false; this.bound = false; this.connected = false; this.closed = false; this.closeLock = new Object(); this.shutIn = false; this.shutOut = false; this.oldImpl = false; this.setImpl(); if (address == null) { throw new NullPointerException(); } else { try { this.createImpl(stream); if (localAddr != null) { this.bind(localAddr); } this.connect(address); } catch (IllegalArgumentException | SecurityException | IOException var7) { try { this.close(); } catch (IOException var6) { var7.addSuppressed(var6); } throw var7; } } }

  ok,终于找到你了,这个构造函数中产生了一个流,先无论这个,能够认为是一个通讯的管道,重点是这里调用了 this.bind(localAddr)和 this.connect(address)方法,是否是很熟悉,没错跟linux socket接口函数同样,一个用来绑定地址并监听,一个用来向服务端请求链接。

  如今再来跟踪下服务端的ServerSocket类和accept()方法:

(1)起始、

ServerSocket s = new ServerSocket(8189)

(2)跟踪ServerSocket、

public ServerSocket(int port) throws IOException { this(port, 50, (InetAddress)null); }

(3)跟踪这个this构造方法:

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException { this.created = false; this.bound = false; this.closed = false; this.closeLock = new Object(); this.oldImpl = false; this.setImpl(); if (port >= 0 && port <= 65535) { if (backlog < 1) { backlog = 50; } try { this.bind(new InetSocketAddress(bindAddr, port), backlog); } catch (SecurityException var5) { this.close(); throw var5; } catch (IOException var6) { this.close(); throw var6; } } else { throw new IllegalArgumentException("Port value out of range: " + port); } }

  能够看到,先判断端口号是否合理,而后调用了 this.bind()方法绑定地址并开始监听这个端口;

(4)跟踪bind()方法:

 

public void bind(SocketAddress endpoint, int backlog) throws IOException { if (this.isClosed()) { throw new SocketException("Socket is closed"); } else if (!this.oldImpl && this.isBound()) { throw new SocketException("Already bound"); } else { if (endpoint == null) { endpoint = new InetSocketAddress(0); } if (!(endpoint instanceof InetSocketAddress)) { throw new IllegalArgumentException("Unsupported address type"); } else { InetSocketAddress epoint = (InetSocketAddress)endpoint; if (epoint.isUnresolved()) { throw new SocketException("Unresolved address"); } else { if (backlog < 1) { backlog = 50; } try { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkListen(epoint.getPort()); } this.getImpl().bind(epoint.getAddress(), epoint.getPort()); this.getImpl().listen(backlog); this.bound = true; } catch (SecurityException var5) { this.bound = false; throw var5; } catch (IOException var6) { this.bound = false; throw var6; } } } } }

 

  发现了什么,bind()函数里又调用了listen()方法;简直和linux socket通讯过程如出一辙啊。

this.getImpl().listen(backlog);

 

  接下来看看 Socket incoming = s.accept()又作了什么:

(1)起始:

Socket incoming = s.accept();

(2)跟踪accept():

public Socket accept() throws IOException { if (this.isClosed()) { throw new SocketException("Socket is closed"); } else if (!this.isBound()) { throw new SocketException("Socket is not bound yet"); } else { Socket s = new Socket((SocketImpl)null); this.implAccept(s); return s; } }

  能够看到accept接受链接并返回一个socket对象,服务端即可利用这个socket对象与客户端通讯。

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

  总结:本次带着你们使用java提供的socket编写了多线程的网络聊天程序,经过对java socket接口调用的一步步跟踪,咱们发现虽然使用java socket编程很是简单,可是其内部也是调用了一系列的如同linux socket通讯的socket函数,废话很少说,用图来直观的感觉一下:

 

 

   上图是服务端的java socket调用过程,即当咱们在java建立一个tcp链接时,须要首先实例化java的ServerSocket类,其中封装了底层的socket()方法、bind()方法、listen()方法。

  客户端java经过使用实例化Socket对象向服务端请求创建链接,在实例化Socket对象时,一样调用了与linux socket API同样的socket()、connect()方法,便可以说是java客户端中的Socket封装了linux socket中的socket()、connect()方法,经过java socket的这种封装屏蔽了底层一些咱们看不到的socket 调用过程,这就对程序员显得更加友好了,可是做为一个计算机专业的学生,咱们不能只使用“黑盒子”,而不去打开“黑盒子”去看看其内部构造,只有挖掘到事物内部、知其然,知其因此然,咱们才能创造属于咱们本身的“黑盒子”!(码了一天了,求支持一波)

相关文章
相关标签/搜索