网络通讯基于请求-响应模型,类似一答一问,也就是通讯的一端发送数据,另一端则反馈数据。
C/S结构:第一次主动发起通讯的程序叫做客户,而在第一次通讯中等待连接的程序叫做服务器。
网络编程中的两种程序就分别是客户(客户端指运行客户程序的机器)和服务器(服务器端指运行服务器程序的机器),比如QQ程序,每个QQ用户安装的是QQ客户端程序,而QQ服务器端程序则运行在腾讯公司的机房中,为大量的QQ用户提供服务。这种网络编程结构成为客户端/服务器结构,也叫作C/S结构。
使用C/S结构的程序,在开发时需要分别开发客户端和服务端。优势在于客户端是专门开发的,可以根据需要实现各种效果,但是通用性差,一种程序的客户端只能和对应的服务器端通讯,而不能和其他服务器端通讯,而如果客户端用浏览器代替,那么浏览器可以同时访问微信的服务端也可以访问QQ邮箱的服务端,这就是区别。
B/S结构:使用通用客户端(如浏览器)的结构叫做浏览器/服务器结构,简称B/S,是一种特殊的C/S结构。
在开发时只需开发服务器端即可(客户端只需要写个图形界面?),但是浏览器的闲置比较大,表现力不强,无法进行系统级操作。
协议:网络编程中最重要的概念。由于网络编程时运行在不同计算机中两个程序之间的数据交换,所以为了让接收端理解该数据,需要规定该数据的格式,这个数据的格式就是协议。
在建立网络连接时需要指定连接到的服务器IP和端口号,建立完成以后,会形成一条虚拟的连接,后续的操作就可以通过该连接实现数据交换了。也就是模拟计算机通信中运输层的功能,它在使用TCP协议传送数据之前,必须先建立一条虚连接,虚连接指的是这并非是一条真正的物理连接,TCP报文段先要传送到IP层,加上IP首部后,再传送到数据链路层。
在Java中,和网络编程有关的Api位于java.net中,Socket类则是其中的一个基础类,它即是用来建立连接的类。
Java对于TCP方式的网络编程提供了良好的支持,Socket类代表客户端连接,ServerSocket类代表服务端连接。
建立连接也就是创建Socket类型的对象,带对象代表网络连接。
Socket socket1 = new Socket(“192.168.1.103”,10000); //IP地址+端口
Socket socket2 = new Socket(“www.sohu.com”,80);
连接到某台主机的某个端口,如果建立连接时本机网络不通,或服务器端程序未开启,则会抛出异常。
通过建立的链接交换数据,必须严格按照请求-响应模型进行,由客户端发送一个请求数据到服务器,服务器反馈一个响应数据给客户端。
Java中数据传输功能由Java IO实现,也就是说只要从链接中获得输入流和输出流即可。
OutputStream os = socket1.getOutputStream(); //获得输出流
InputStream is = socket1.getInputStream(); //获得输入流
后续的操作就变成了IO操作。我们可以先向输出流中写入数据,这些数据会被系统发送出去,然后再从输入流中读取服务器的反馈信息,这样就完成了一次数据交换过程。
释放程序占用的端口、内存等系统资源,结束网络编程。
socket1.close();
示例:一个简单的网络客户端程序示例,作用是向服务器端发送一个字符串“Hello”,并将服务器端的反馈显示到控制台。不过现在还不能正常运行,需要和等下的服务端同步运行才能监听到10000.
public static void main(String[] args) {
Socket socket=null;
InputStream is=null;
OutputStream os=null;
//服务器ip地址
String serverIp="127.0.0.1";
//服务器端端口号
int port=10000;
//发送内容
String data="Hello";
try {
//建立连接
socket=new Socket(serverIp,port);
//发送数据
os=socket.getOutputStream();
os.write(data.getBytes());
//接收反馈数据
//输入流的获取可以在连接建立后任一步,不一定非要在发送完数据后
is=socket.getInputStream();
byte[] b=new byte[1024];
int n=is.read(b);
System.out.println("服务器反馈: "+new String(b,0,n));
} catch (IOException e) {
e.printStackTrace();
} finally{
try {
//关闭流和链接
is.close();
os.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个端口就是服务器开放给客户端的端口,也就是监听是否有客户端连接到达。
//实现服务器监听
ServerSocket ss = new ServerSocket(10000);
如果10000端口已被占用,则抛出异常。
获得当前到达的服务器的客户端连接。
Socket socket=ss.accept();
接下来和客户端交换数据就通过这个连接了。
当服务器程序关闭时,关闭服务端连接。
ss.close();
实例:实现一个echo服务器,echo的意思是回声,就是将客户端发送的内容原封不动地反馈给客户端。先运行该程序,再运行客户端。
public static void main(String[] args) {
ServerSocket serverSocket=null;
Socket socket=null;
OutputStream os=null;
InputStream is=null;
//监听端口号
int port=10000;
try {
//建立连接
serverSocket=new ServerSocket(port);
//获得客户端连接;该方法是阻塞方法,进入这一步的执行时会等待客户端
socket=serverSocket.accept();
//接收客户端发送内容
is=socket.getInputStream();
byte[] b=new byte[1024];
int n=is.read(b);
System.out.println("客户端发送内容:"+new String(b,0,n));
//反馈
os=socket.getOutputStream();
os.write(b,0,n);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭流和连接
is.close();
os.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
实际的服务器需要同时支持多个客户端工作,我们可以通过多线程来实现。
实际上服务端的accept()获得当前客户端的连接后,即释放了当前客户端对该端口的占用,服务器可以继续监听其他连接的到来,但如果只有一个线程,那么处理完当前请求程序也就结束了,端口自然也没人监听了,所以必须结合循环和线程才能实现继续监听并处理下一个连接的功能。
MulThreadSocketServer:在主程序中监听端口,然后设置一个循环来无限次地接收和处理请求,接收请求的操作是公共的,而处理操作的请求是各异的,需要开一个线程来分开执行。
public class MultiThreadSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket=null;
Socket socket=null;
int port=10000;
try {
//监听端口
serverSocket=new ServerSocket(port);
System.out.println("服务器已启动:");
while (true){
System.out.println("收到一个请求...");
//获得连接
socket=serverSocket.accept();
//开线程处理请求
new LogicThread(socket);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭连接
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* @Author haien
* @Description 用于处理请求的线程
* @Date 2019/6/26
**/
public class LogicThread extends Thread {
Socket socket;
InputStream is;
OutputStream os;
public LogicThread(Socket socket) {
this.socket = socket;
//启动线程
start();
}
@Override
public void run() {
byte[] b=new byte[1024];
try {
os=socket.getOutputStream();
is=socket.getInputStream();
//进行多次数据交换,客户端应该相应地也发出这么多次数据
for (int i=0; i<3; i++){
//接收数据
int n=is.read(b);
//处理数据
byte[] response=logic(b,0,n);
//反馈
os.write(response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭流和连接
close();
}
}
/**
* @Author haien
* @Description 处理数据,实际就是直接拷贝
* @Date 2019/6/26
* @Param [b, start, len]
* @return byte[]
**/
public byte[] logic(byte[] b,int start,int len){
byte[] response=new byte[len];
//直接拷贝数组
System.arraycopy(b,0,response,0,len);
return response;
}
/**
* @Author haien
* @Description 关闭流和连接
* @Date 2019/6/26
* @Param []
* @return void
**/
public void close(){
try {
is.close();
os.close();
socket.close();
} catch (IOException e) {
}
}
}
从LogicThread可以看出,该服务器匹配进行三次数据交换的客户端。
MulSocketClient:发起三次数据交换。
public class MulSocketClient {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
String serverIP = "127.0.0.1";
int port = 10000;
//发送内容
String data[] ={"First","Second","Third"};
try {
//建立连接
socket = new Socket(serverIP,port);
os = socket.getOutputStream();
is = socket.getInputStream();
byte[] b = new byte[1024];
for(int i = 0;i < data.length;i++){
//发送数据
os.write(data[i].getBytes());
//接收数据
int n = is.read(b);
//输出反馈数据
System.out.println("服务器反馈:" + new String(b,0,n));
}
} catch (Exception e) {
e.printStackTrace(); //打印异常信息
}finally{
try {
//关闭流和连接
is.close();
os.close();
socket.close();
} catch (Exception e2) {}
}
}
}
MulSocketClient2:第二个客户端,把发送数据改一下就行。
先执行服务端程序,再执行两个客户端,可以看到两个客户端都收到了反馈。
但是客户端和服务器的数据交换次数还不够灵活,不应该卡死在三次,如果客户端的次数不固定怎么办呢?我们可以使用某个特殊的字符串,表示客户端退出,不过这就涉及到网络协议的内容了。
在实际的服务器中,由于硬件和端口数的限制,不能无限制地创建线程对象,实际上频繁地创建线程对象效率也比较低,所以程序中都实现了线程池来提高程序的执行效率。
线程池:Thread pool,是池基数的一种,就是在程序启动时首先把所需数目的线程对象创建好,然后当客户端连接到达时从池中取出一个线程对象来使用。当客户端连接关闭以后,将该线程对象重新放入线程池中供其他客户端复用,这样可以提供程序的执行速度,优化程序对于内存的占用等。
实际就是如何解析接收到的数据的一种约定,服务端如何理解客户端发来的数据呢,客户端又如何解析服务端反馈的数据呢?协议就做了规定,对于程序实现来说,也就是一些判断、选择、处理代码了,比如,判断客户端发来的数字是否为0,为0则反馈一个-1表示参数异常,而客户端接收到反馈则给它们分类,1是成功,-1是失败。
参考文章(https://blog.csdn.net/sihai12345/article/details/79334299)
代码实例:ideaProjects/jar-test/socket