在当今互联网时代,网络显得尤为重要,不论是QQ、微信,还是网络游戏,都离不开网络通信,而java作为当web开发最火的语言,相信大家都接触过java网络编程,那java网络通信中调用了系统级的哪些接口呢?今天,我就带着大家共同探究java socket与linux socket之间的千丝万缕。
说到网络通信怎么能不谈计算机网络呢,简而言之,网络界主要有两种网络分层模型:即OSI和TCP/IP,OSI有7层,TCP/IP则将网络分为4层,现在TCP/IP模型是事实上的网络标准,而我们结合两者,一般都说TCP/IP 5层协议模型,下面给一张图来说明:
那socket接口在哪一层呢,事实上socket是系统为我们提供的网络通信接口,如果非要说它在哪一层的话,那么socket就位于应用层和传输层之间,通过socket接口对网络的抽象,屏蔽了下面各层那么多复杂的协议,给人感觉好像是用socket套接字直接与对方通信一样,这样大大简化了程序员的工作,使得程序员根本不需要关心底层的东西,只需要通过socket接口与应用层和传输层打交道即可,当然其实大部分时间程序员只需要关心应用层即可。一般来讲,传输层有两大协议,即面向连接的TCP和无连接的UDP协议,所谓面向连接是指传输是有序的、无差错的,可能更费时,但很有用;而无连接是指尽最大努力交付,出点差错也无所谓。
socket会用到运输层的服务,那么当然socket接口也有基于tcp的socket和基于udp的socket之分,udp比较简单,今天就以基于tcp的socket为例,使用java语言编写一个socket多线程网络聊天程序,探究java socket背后的工作原理。
在编写代码之前先简单介绍下java网络编程中最重要的socket接口:
1、Scoket又称“套接字”,其由IP地址和端口号组成,可以说它唯一标识了网络上的某个进程,应用程序通常通过“套接字”向网络发出请求或者应答网络请求;在 java中Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是建立网络连接时使用的,在连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同的级别,不管是Socket还是ServerSocket他们的工作都是通过Socket类和其子类来完成的
2、建立Socket链接可分三个步骤:
1.服务器监听
2.客户端发出请求
3.建立链接
4.通信
3、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源码:
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 /* See NOTES */
#include
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 /* See NOTES */
#include
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 /* See NOTES */
#include
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 /* See NOTES */
#include
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 /* See NOTES */
#include
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 调用过程,这就对程序员显得更加友好了,但是作为一个计算机专业的学生,我们不能只使用“黑盒子”,而不去打开“黑盒子”去看看其内部构造,只有挖掘到事物内部、知其然,知其所以然,我们才能创造属于我们自己的“黑盒子”!(码了一天了,求支持一波)