1. 引言
socket网络编程,可以指定不同的通信协议,在这里,我们使用TCP协议实现基于java的C/S模式下“hello/hi”网络聊天程序
2. 目标
1). 通过该网络聊天程序,了解java Socket API接口的基本用法
2). java Socket API简要介绍
3). linux socket API 简单分析
4). tcp协议的连接和终止
5). 探究java socket API 和 linux socket api之间的关系
3. linux socket API简单分析
1). 简要介绍一下 tcp协议
tcp协议提供了可靠的面向连接的字节流运输层服务协议, 它将用户数据打包生成一个报文段发送出去,同时启动一个定时器;接收端则会对数据进行确认,重排失序的数据,并丢弃重复数据。
tcp面向连接,意味着每次发送数据要先在两端间建立连接; 终止发送也要关闭连接
a). 建立连接
客户端: 请求发送方; 服务端: 数据接收方
tcp通过三次握手来建立连接
客户端先发送一个报文段,执行主动打开 --> 服务器发回一个SYN报文段作为应答 --> 客户端再发送一个报文段对服务器的应答进行确认
b). 终止连接
tcp需要四次握手来关闭连接,因为tcp连接时全双工,两个方向上的都要关闭,而每次的关闭都需要确认,由此需要四次握手
2). API 分析
a). 建立和终止
客户端在调用connect()方法时候,会进行三次握手来建立连接,只有连接建立成功/失败的时候才会返回
在此之前,服务端会一直阻塞等待客户端的请求
当连接建立成功的时候,客户端和服务端进行数据传输
传输结束后,客户端调用close()方法,
b). bind
会将本地协议地址赋予一个套接口
c). listen
当服务端调用socket(),会假定这是一个主动连接(即将调用connect发起连接的客户套接口),而listen()则将其转为被动连接,让内核接收指向该套接口的连接请求
内核维护两个队列:未完成连接队列,已完成连接队列
d). accept
当客户端connect的时候,会在服务端的未完成连接队列中创建一个条目,当连接完成时候,将该条目移动到已完成连接队列中
accept则从已完成连接队列中拿到一个已完成连接
4. 基于java的“hello/hi”网络聊天程序的简单实现
1 import java.io.*; 2 import java.net.ServerSocket; 3 import java.net.Socket; 4 5 public class Server { 6 public static void main(String[] args) throws Exception { 7 System.out.println("服务端启动 , 等待连接 .... "); 8 // 1. 创建 ServerSocket对象,绑定端口,开始等待连接 9 ServerSocket server = new ServerSocket(6666); 10 // 2. 接收连接 accept 方法, 返回 socket 对象; 此方法在连接传入之前一直阻塞,返回新的套接字 11 Socket socket = server.accept(); 12 // 3. 获取输入流 13 InputStream is = socket.getInputStream(); 14 // 4. 一次性读取数据 15 byte[] b = new byte[1024]; 16 int len = is.read(b); 17 // 解析数组 18 String msg = new String(b, 0, len); 19 System.out.println("From Client-> "+msg); 20 21 // 5. 获取输出流, 向客户端写入数据 22 OutputStream out = socket.getOutputStream(); 23 out.write("Hi".getBytes()); 24 // 关闭资源 25 out.close(); 26 is.close(); 27 server.close(); 28 } 29 }
1 import java.io.*; 2 import java.net.Socket; 3 4 public class Client { 5 public static void main(String[] args) throws Exception { 6 System.out.println("客户端 发送数据"); 7 // 1. 创建 Socket , 确定连接到哪里 8 Socket client = new Socket("localhost", 6666); 9 // 2. 获取输出流 10 OutputStream os = client.getOutputStream(); 11 // 3. 写出数据 12 os.write("Hello".getBytes()); 13 // 4. 获取输入流,获取服务端返回的信息 14 InputStream in = client.getInputStream(); 15 16 // 5.读取服务端返回的数据 17 byte[] b = new byte[100]; 18 int len = in.read(b); 19 System.out.println("From Sever-> "+new String(b, 0, len)); 20 // 5. 关闭资源 21 in.close(); 22 os.close(); 23 client.close(); 24 } 25 }
5. java socket API 和 linux socket api 关系探究
由上面的程序,我们可以观察到java中 server端相比linux下少了bind()和listen()两个方法, 客户端也没有connect()方法
我们看一下Java和Linux Sever端API 对应关系,下面是网上的一张图,网络来源(https://blog.csdn.net/vipshop_fin_dev/article/details/102966081)
当运行在linux系统上的时候,jvm会调用linux底层socket()函数,我们比较一下服务端的对应API
a. ServerSocket server = new ServerSocket(6666); 会先创建一个socket,而其底层(对应的linux api)是实现了下面三个过程:
1. 创建socket
若要执行网络I/O,进程首先就要调用一个socket函数,并会指定一个期望的协议类型和套接口类型
socket函数成功的时候会返回一个小的非负整数, 因在Linux中一切皆文件,所以这个整数表示一个文件描述符
此时会创建socket对应的结构,并将其和一个已经打开的文件对应
2. 将对应socket和本地协议地址对应(bind函数)
对于TCP协议,bind函数可以和一个IP和一个端口绑定;此时该套接口处于TCP_CLOSE状态
如果不绑定端口号,则内核会为该套接口绑定一个临时的端口
如果不绑定IP地址,对于服务端而言,内核就会将客户端发送的SYN的目的IP地址作为服务器的源IP地址
3. listen()监听
由上面可以知道,listen会将主动转为被动连接,会将套接口由closed状态转换为linsten状态;并且维护了两个队列
此时会和客户端进行三次握手来建立连接
b. Socket socket = server.accept();
linux中由TCP服务器调用,从已完成的连接队列的队头返回下一个已完成连接;成功则会返回由内核创建的新的描述符;
在并发服务器中,accept()返回后,服务器会调用fork函数,创建一个子进程,由该子进程来和客户端进行通讯,此时套接口对应文件描述符的引用计数会增加
同时父进程会关闭该已连接套接字,即会将引用计数减少。然后在原有的监听套接口上,继续监听有无其他连接,当由连接的时候,再次accept处理连接
c. server.close();
当待关闭的socket的描述符的引用计数为0的时候才会真正的关闭连接
参考来源:
1. https://blog.csdn.net/vipshop_fin_dev/article/details/102966081
2. 《UNIX网络编程》
原文地址:https://www.cnblogs.com/zhouz/p/11980538.html