CS架构
C/S结构
无论使用TCP方式还是UDP方式进行网络通讯,网络编程都是由客户端和服务器端组成。当然,B/S结构的编程中只需要实现服务器端即可。以下以C/S结构为基础进行介绍。
网络编程步骤
说明:这里的步骤实现和语言无关,也就是说,这个步骤适用于各种语言实现,不局限于Java语言
客户端网络编程步骤
客户端(Client)是指网络编程中首先发起连接的程序,客户端的编程主要由三个步骤实现:
- 建立网络连接
客户端网络编程的第一步都是建立网络连接。在建立网络连接时需要指定连接到的服务器的IP地址和端口号,建立完成以后,会形成一条虚拟的连接,后续的操作就可以通过该连接实现数据交换了。 - 交换数据
连接建立以后,就可以通过这个连接交换数据了。交换数据严格按照请求响应模型进行,由客户端发送一个请求数据到服务器,服务器反馈一个响应数据给客户端,如果客户端不发送请求则服务器端就不响应。
根据逻辑需要,可以多次交换数据,但是还是必须遵循请求响应模型。 - 关闭网络连接
在数据交换完成以后,关闭网络连接,释放程序占用的端口、内存等系统资源,结束网络编程。
最基本的步骤一般都是这三个步骤,在实际实现时,步骤2会出现重复,另外网络编程是比较耗时的操作。
服务器端网络编程步骤
服务器端(Server)是指在网络编程中被动等待连接的程序,服务器端一般实现程序的核心逻辑以及数据存储等核心功能。服务器端的编程步骤和客户端不同,是由四个步骤实现,依次是:
- 监听端口
服务器端属于被动等待连接,所以服务器端启动以后,不需要发起连接,而只需要监听本地计算机的某个固定端口即可。
这个端口就是服务器端开放给客户端的端口,服务器端程序运行的本地计算机的IP地址就是服务器端程序的IP地址。 - 获得连接
当客户端连接到服务器端时,服务器端就可以获得一个连接,这个连接包含客户端的信息,例如客户端IP地址等等,服务器端和客户端也通过该连接进行数据交换。
一般在服务器端编程中,当获得连接时,需要开启专门的线程处理该连接,每个连接都由独立的线程实现。 - 交换数据
服务器端通过获得的连接进行数据交换。服务器端的数据交换步骤是首先接收客户端发送过来的数据,然后进行逻辑处理,再把处理以后的结果数据发送给客户端。简单来说,就是先接收再发送,这个和客户端的数据交换数序不同。
其实,服务器端获得的连接和客户端连接是一样的,只是数据交换的步骤不同。
当然,服务器端的数据交换也是可以多次进行的。
在数据交换完成以后,关闭和客户端的连接。 - 关闭连接
当服务器程序关闭时,需要关闭服务器端,通过关闭服务器端使得服务器监听的端口以及占用的内存可以释放出来,实现了连接的关闭。
InetAddress和URL类
InetAddress
InetAddress类用于标识网络上的硬件资源,标识互联网协议(IP)地址。 该类没有构造方法。
//获取本机的InetAddress实例
InetAddress address = InetAddress.getLocalHost();
address.getHostName(); // 获取计算机名
address.getHostAddress(); // 获取IP地址
byte[] bytes = address.getAddress(); // 获取字节数组形式的IP地址,以点分隔的四部分
// 获取其他主机的InetAddress实例
InetAddress address2 = InetAddress.getByName("其他主机名");
InetAddress address3 = InetAddress.getByName("IP地址");
URL类
-
URL(Uniform Resource Locator)统一资源定位符,表示Internet上某一资源的地址,协议名:资源名称
//创建一个URL的实例 URL baidu = new URL("http://www.baidu.com"); URL url = new URL(baidu, "/index.html?username=tom#test"); // ?表示参数,#表示锚点 url.getProtocol(); // 获取协议 url.getHost(); // 获取主机 url.getPort(); // 如果没有指定端口号,根据协议不同使用默认端口。此时getPort()方法的返回值为 -1 url.getPath(); // 获取文件路径 url.getFile(); // 文件名,包括文件路径+参数 url.getRef(); // 相对路径,就是锚点,即#号后面的内容 url.getQuery(); // 查询字符串,即参数
- 使用URL读取网页内容
//使用URL读取网页内容 URL url = new URL("http://www.baidu.com"); InputStream is = url.openStream(); // 通过openStream方法获取资源的字节输入流 InputStreamReader isr = new InputStreamReader(is, "UTF-8"); // 将字节输入流转换为字符输入流,如果不指定编码,中文可能会出现乱码 BufferedReader br = new BufferedReader(isr); // 为字符输入流添加缓冲,提高读取效率 String data = br.readLine(); // 读取数据 while (data != null) { System.out.println(data); // 输出数据 data = br.readLine(); } br.close(); isr.close(); is.close();
socket编程
-
在客户/服务器通信模式中,客户端需要主动建立与服务器连接的Socket,服务器端收到客户端的连接请求,也会创建与客户端连接的Socket。Socket可以看做是通信连接两端的收发器,客户端和服务店都通过Socket来收发数据。
-
c/s编程一般基于TCP协议:TCP协议是面向连接的、可靠的、有序的、以字节流的方式发送数据,通过三次握手方式建立连接,形成传输数据的通道,在连接中进行大量数据的传输,效率会稍低
-
Java中基于TCP协议实现网络通信的类:客户端的Socket类;服务器端的ServerSocket类
socket通信模型如下图: -
Socket通信的步骤:
- 创建ServerSocket和Socket
- 打开连接到Socket的输入/输出流
- 按照协议对Socket进行读/写操作
- 关闭输入输出流、关闭Socket
服务端socket代码
- 创建ServerSocket对象,绑定监听端口
- 通过accept()方法监听客户端请求
- 连接建立后,通过输入流读取客户端发送的请求信息
- 通过输出流向客户端发送乡音信息
- 关闭相关资源
public class Sever {
public static void main(String[] args) throws IOException {
// 1、创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket = new ServerSocket(10086);
System.out.println("服务器已启动。");
while (true) {
// 2、调用accept()方法开始监听,等待客户端的连接,,一直阻塞在此处,需要客户端连接才会让accept从阻塞态唤醒
Socket socket = serverSocket.accept();
// 3、获取输入流,并读取客户端信息
InputStreamReader isr = new InputStreamReader(socket.getInputStream(), "UTF-8");
BufferedReader br = new BufferedReader(isr);
String info;
while ((info = br.readLine()) != null) {
System.out.println("客户端:" + info);
}
socket.shutdownInput();
// 4、获取输出流,响应客户端的请求
OutputStreamWriter osw = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
PrintWriter pw = new PrintWriter(osw);
pw.write("欢迎您!");
pw.flush();
// 5、关闭资源
pw.close();
osw.close();
br.close();
isr.close();
socket.close();
}
}
}
客户端socket代码
- 创建Socket对象,指明需要连接的服务器的地址和端口号
- 连接建立后,通过输出流想服务器端发送请求信息
- 通过输入流获取服务器响应的信息
- 关闭响应资源
public class Client {
public static void main(String[] args) throws IOException {
while (true) {
//1、创建客户端Socket,指定服务器地址和端口
Socket socket = new Socket("localhost", 10086);
//2、获取输出流,向服务器端发送信息
OutputStreamWriter osw = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
PrintWriter pw = new PrintWriter(osw);
pw.write(new Scanner(System.in).nextLine());
pw.flush();
socket.shutdownOutput();
//3、获取输入流,并读取服务器端的响应信息
InputStreamReader isr = new InputStreamReader(socket.getInputStream(), "UTF-8");
BufferedReader br = new BufferedReader(isr);
String info;
while ((info = br.readLine()) != null) {
System.out.println("服务器:" + info);
}
//4、关闭资源
br.close();
isr.close();
pw.close();
osw.close();
socket.close();
}
}
}
应用多线程实现服务器与多客户端之间的通信
客户端和服务器之间只有一个通讯线程,所以它们之间只有一条Socket信道。如果我们在通过程序里引入多线程的机制,可让一个服务器端同时监听并接收多个客户端的请求,并同步地为它们提供通讯服务。基于多线程的通讯方式,将大大地提高服务器端的利用效率,并能使服务器端能具备完善的服务功能
- 服务器端创建ServerSocket,循环调用accept()等待客户端连接
- 客户端创建一个socket并请求和服务器端连接
- 服务器端接受苦读段请求,创建socket与该客户建立专线连接
- 建立连接的两个socket在一个单独的线程上对话
- 服务器端继续等待新的连接
public class SocketServer {
public static void main(String[] args) throws Exception {
SocketServer socketServer = new SocketServer();
socketServer.domain();
}
public void domain() throws IOException {
ServerSocket s = new ServerSocket(10086);
System.out.println("The Server is start: " + s);
try {
for (; ; ) {
//阻塞,直到有客户端连接
Socket socket = s.accept();
//通过构造函数,启动线程
new ServerThread(socket);
}
} finally {
s.close();
}
}
public class ServerThread extends Thread {
//服务器线程处理
//和本线程相关的socket
private Socket socket;
//IO句柄
private BufferedReader sin;
private PrintWriter sout;
public ServerThread(Socket socket) throws IOException {
this.socket = socket;
//初始化sin和sout的句柄
sin = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
sout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream(), "UTF-8")), true);
//开启线程
start();
}
@Override
public void run() {
//服务器处理代码
try {
System.out.println("thread id:" + this.getId());
String info = null;
while ((info = sin.readLine()) != null) {
System.out.println("我是服务器,客户端说:" + info);
}
socket.shutdownInput();
//输出信息
sout.write("欢迎您!");
sout.flush();
System.out.println("closing the server socket!");
} catch (IOException ex) {
ex.printStackTrace();
} finally {
System.out.println("close the Server socket and the io.");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
注意
- 基于tcp协议的socket编程一般应用于长连接,一次建立连接后一直发送数据,直到数据两边处理完毕,比如安卓游戏,与服务端交互就是长连接
- 另外基于长连接,服务端和客户端可以双边发送数据,保证了数据的及时到达,比如要A系统有变化了,B系统需要马上知道,此时使用长连接就比较合适
- UDP协议的网络编程同学们工作中如果有需要了再进行学习
- 实际使用的长连接比上面的DEMO要复杂的多,同学们入职后再根据工作需要进行学习