1. 前言
簡介
网络编程中最基本的架构是C/S架构,也就是由客户端/服务端结合而成的连线架构,而C/S架构又有多种不同的实现方案,对应不同情况下的需求衍生出多种不同效能的网路模型实现方式可供选择,
在程序运行的角度所处的情境下一般可以分为以下四种:
(1) 同步阻塞:由功能调用的调用者(线程)等待结果返回,在结果返回前,当前线程被挂起阻塞住不往下面运行。如:read。
(2) 同步非阻塞:由功能调用的调用者(线程)等待结果返回,调用无法立即得到结果时会立即返回一个状态值,不会阻塞住当前线程运行。但是程序需要循环调用处理,等待接收到数据时才可以开始处理数据。
(3) 异步阻塞:由事件通知机制来通知调用者(线程)所关注的事件被触发,可以开始发起功能调用接收数据。如:select,使用时IO操作通常设为非阻塞,但select调用会阻塞住线程,所以从程序运行的角度來看是阻塞的。
(4) 异步非阻塞:在使用select时仍然需要等待事件通知,线程会被阻塞在select调用上,算是半残的异步,因此有了优化的IO模型如IOCP、AIO,当线程收到事件通知时,数据已经接收完毕并且在用户线程指定的buffer中,线程可直接进行其他处理。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
补充:同步/异步、阻塞/非阻塞 之厘清
通常阻塞非阻塞的观念其实不容易混淆,比较容易混淆的是同步异步,他们在不同的情境下会有不同的意义。
以下给出一个从I/O操作角度来看的定义,也就是在I/O模型下的定义:
# 同步、异步:指的是 或者说关注的是 调用者(线程)与被调用者(内核)的交互方式。
同步:由"(功能)调用"的调用者(线程)等待"调用"的结果,调用有得到被调用者(內核)返回之结果时才会返回到线程继续运行。
*线程要等待调用结果返回才能往下运行。(此时线程还是激活的)
异步:"(功能)调用"发起后就直接返回,所以不会立即得到返回之结果,
要等到被调用者(内核)I/O操作完成后,藉由"状态"、"通知"来通知调用者(线程),或调用调用者(线程)早先注册的回调函数。
*线程在发起调用后仍继续运行。
*注:
在I/O模型的观点下,select调用算是同步I/O,
因为select虽然有类似异步阻塞的通知机制,但还是要由线程主动发起读写I/O读写数据,整个程序是按顺序运行;
而真正的异步I/O则是由内核读写完数据才通知线程,不需要由线程主动读写数据。在程序运行的角度来看,实际处理调用的时机是只要是在等到被调用者(内核) 藉由"状态"、"通知"来通知调用者(线程)进行处理,
或调用调用者(线程)早先注册的回调函数,就可以看做异步。
至于在进程间、线程间、网路通信间的情境下,同步又是另一回事了。
# 阻塞、非阻塞:指的是 调用者(线程)进行调用及等待调用结果的方式。
阻塞:"(功能)调用"发起后,在I/O操作完成之前 且 调用有得到返回之结果前,线程会被挂起(等待调用得到返回结果),
直到I/O操作完成后返回结果给调用后,线程才会恢复继续运行。
非阻塞:"(功能)调用"发起后,I/O操作被调用后会返回一个状态值(给调用),所以调用会得到返回结果,线程不会被挂起阻塞住。
=====================================================================================
*同步and异步差别:同步I/O过程是顺序执行的,异步I/O不是顺序执行。同步I/O中线程要等待调用结果返回,异步则否。
*同步and阻塞差别:同步调用发生时,当前线程是激活的,只不过调用(函数)还没返回而已;阻塞则是线程被挂起。
网路I/O模型一般分为四种:
|----阻塞I/O
同步---|----非阻塞I/O
|----I/O复用(multiplexing) (select/poll/epoll)
异步I/O (AIO、IOCP)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
废话不多说了,概念性质的话讲再多也没法体会,本篇中将给出同步阻塞的代码实现实例。
2. 服务端代码
简单介绍一下服务端的实现, 服务端程序在指定的端口上开启监听,然后用回圈循环监听来自客户端的连接请求。当接收到连接请求时,给客户端分配一个socket来识别对此客户端的操作。
一般来说,同步阻塞的实例到这里就差不多完成了,后续就是开始做网路数据的IO处理,只不过这样一来只能进行一对一的连线通信,因为主线程在接收完连线请求后就开始做IO处理了;
如果我们想扩展服务端的网路规模,我们可以通过修改软体架构来实现,最简单的实现方式是可以为每个客户端连线开启一个独立的线程为它服务,这样一来服务端就可以达成一对多客户端服务的效果。
但此种方式的缺陷在于服务可扩展的规模有极限,受限于硬件资源以及系统限制,通常如windows操作系统在32位元下会限制每个进程的内存大约2g,而线程可默认分配到1m的空间,如此一来,每个进程理论上只能开启2000个子线程左右,对应到大约2000个客户端;
2000看似对于小型服务来说是够用的,但是实际情况下CPU每个核心在执行线程时需要给线程分配处理时间,会额外增加系统开销,线程越多系统负载越重,程序效率越差,所以并不是线程开越多就能应付越复杂的情况 :-(
在这篇同步阻塞的Java示例中,以多线程应付简单情况下的网路通信已经足够,要让网络程序更有效率的运行则需要改用其他的网路模型,有机会的话将在后续的文章中介绍。
主線程代碼
[Server.java]
package main.pkg;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class Server {
private ServerSocket msock;
public Server(int port) throws IOException {
msock = new ServerSocket(port); // 在端口上开启监听
msock.setSoTimeout(0);
while(true) { // 循环监听连线请求
System.out.println("Waiting for new client on port " + msock.getLocalPort() + "...");
try {
Socket ssock = msock.accept(); // 接受client连线
new ClientThread(ssock).start(); // 为client开新处理线程
} catch (SocketTimeoutException e) {
System.out.println("Socket accepting time-out!");
break;
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int port = Integer.parseInt(args[0]);
//int port = 1333;
try {
Server server=new Server(port);
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务客户端之子线程代码
[ClientThread.java]
package main.pkg;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
public class ClientThread extends Thread{
private Socket s;
public ClientThread(Socket ssock) {
s=ssock;
}
@Override
public void run() {
super.run();
System.out.println(s.getRemoteSocketAddress() + " is connected.");
try {
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter out = new PrintWriter(s.getOutputStream());
String text=null;
while((text=in.readLine())!=null && !text.equals("exit")) { // 若读入字串不为空且不为exit,则印出client发来的字符串
System.out.println("client " + s.getRemoteSocketAddress() +" > " + text);
out.println("Your message has sent to " + s.getLocalSocketAddress()); // 回传给client回应字符串
out.flush(); // 强制将缓冲区内的数据输出
}
System.out.println(s.getRemoteSocketAddress() + " is closed.");
s.close();
} catch (SocketException e) {
System.out.println("[Warning!] A client had been closed.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 客户端代码
[Client.java]
package main.pkg;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client {
public static void main(String[] args) {
String hostname=args[0];
int port = Integer.parseInt(args[1]);
//String hostname="127.0.0.1";
//int port=1333;
System.out.println("Connecting to "+ hostname +":"+port);
try {
Socket client = new Socket(hostname, port); // 连接至目的地
System.out.println("Connected to "+ hostname);
PrintWriter out = new PrintWriter(client.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String input;
while((input=stdIn.readLine()) != null) { // 读取输入
out.println(input); // 发送输入的字符串
out.flush(); // 强制将缓冲区内的数据输出
if(input.equals("exit"))
{
break;
}
System.out.println("server: "+in.readLine());
}
client.close();
System.out.println("client stop.");
} catch (UnknownHostException e) {
System.err.println("Don't know about host: " + hostname);
} catch (IOException e) {
System.err.println("Couldn't get I/O for the socket connection");
}
}
}