一、Socket简单介绍
Socket通信作为Java网络通讯的基础内容,集中了异常、I/O流模式等众多知识点。学习Socket通信,既能够了解真正的网络通讯原理,也能够增强对I/O流模式的理解。
1)Socket通信分类
(一)基于TCP的Socket通信:使用流式套接字,提供可靠、面向连接的通信流。
(二)基于UDP的Socket通信:使用数据报套接字,定义一种无连接服务,数据之间通过相互独立的报文进行传输,是无序的,并且不保证可靠、无差错。
2)Socket概念理解
金山词霸中对Socket名词解释:插座、灯座、窝,引申到计算机科学称为”套接字”。至于为什么要翻译成”套接字”,可以参考:https://www.zhihu.com/question/21383903/answer/18347271z对Socket历史较为详细考证。
Socket曾经被翻译为”软插座”,表明此处说的插座不是实际生活中的那种插座(硬插座),而是在计算机领域抽象出来的接口。如果在客户端插座和服务器端插座之间连一条线(也就是数据交互的信道),那么客户端就能够与服务器端进行数据交互。
二、基于TCP的Socket通信理论基础
基于TCP/IP协议的网络编程,就是利用TCP/IP协议在客户端和服务器端之间建立通信链接来实现数据交换。 具体的编程实现步骤如下:
1)服务器端创建其提供服务的端口号,即服务器端中提供服务的应用程序接口名称。
服务器端ServerSocket: ServerSocket serverSocket = new ServerSocket(int port, int backlog); ServerSocket作用是向操作系统注册相应协议服务,申请端口并监听这个端口是否有链接请求。其中port是端口号,backlog是服务器最多允许链接的客户端数。注册完成后,服务器分配此端口用于提供某一项进程服务。
2)服务器端(Server)和客户端(Client)都创建各自的Socket对象。
服务器端Socket: Socket socket = serverSocket.accept(); 服务器端创建一个socket对象用于等待客户端socket的链接(accept方法是创建一个阻塞队列,只有客户端socket申请链接到服务器后,服务器端socket才能收到消息) 。如果服务器端socket收到客户端的链接请求,那么经过”三次握手”过程,建立客户端与服务器端的连接。如果连接不成功,则抛出异常(详见模块三)。
客户端Socket: Socket socket = new Socket(String host, int port); 客户端创建按一个socket对象用于链接具体服务器host的具体服务端口port,用于获得服务器进程的相应服务。
经过三次握手后,一个Socket通路就建立起来。此时,服务器端和客户端就可以开始通讯了。
3)服务器端和客户端打开链接到Socket通路的I/O流,按照一定协议进行数据通信。
协议就是指发送与接受数据的编码格式(计算机网络中为:语义、同步)。简单说就是输入和输出的流必须匹配。
开启网络输入流:网络输入流指的是从socket通道进入计算机内存的流。 socket.getInputStream(); 返回值InputStream 输入字节流
开启网络输出流:网络输出流指的是从计算机内存走出到socket通道的流。 socket.getOutputStream(); 返回值OutputStream 输出字节流
为了通讯方便,往往将低级流包装成高级流进行服务端与客户端之间的交互。
4)通信完毕,关闭网络流
一般而言,服务器端的流失不用关闭的,当然在某些条件下(比如服务器需要维护)也是需要关闭的。而客户端一般都需要关闭。
三、Socket异常类
网络通讯中会遇到很多种错误,比如通讯中断、服务器维护拒绝访问等等。下面稍微总结一下Socket通讯中常见的异常类。
1)java.net.SocketTimeoutException套接字超时异常。常见原因:网络通路中断,链接超时;
2)java.net.UnknowHostException未知主机异常。常见原因:客户端绑定的服务器IP或主机名不存在;
3)java.net.BindException绑定异常。常见原因:端口被占用;
4)java.net.ConnectException连接异常。常见原因:服务器未启动,客户端申请服务;服务器拒绝服务,即服务器正在维护;
四、Java建立Socket通讯
1)服务器端与客户端建立连
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
* @author forget406
*
*/
public class Server {
private ServerSocket serverSocket;
/** 在操作系统中注册8000端口服务,并监听8000端口 */
public Server() {
try {
/* public ServerSocket(int port, int backlog)
* port表示端口号,backlog表示最多支持连接数 */
serverSocket = new ServerSocket(8000, 3);
} catch (IOException e) {
e.printStackTrace();
}
}
/** 与客户端交互 */
public void start() {
try {
System.out.println("等待用户链接...");
/* 创建Socket对象: public Socket accept()
* 等待客户端链接,直到客户端链接到此端口 */
Socket socket = serverSocket.accept();
System.out.println("链接成功,可以通讯!");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
==============================================
package day05;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* 客户端
* @author forget406
*
*/
public class Client {
private Socket socket;
/** 申请与服务器端口连接 */
public Client() {
try {
/* 请求与服务器端口建立连接
* 并申请服务器8000端口的服务*/
socket = new Socket("localhost", 8000);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/** 与服务器交互 */
public void start() {
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
五、Java实现C/S模式Socket通讯
1)客户端向服务器端发送消息(单向通信):服务器只能接受数据,客户端只能发送数据。这是由于socket绑定了从客户端到服务器的一条通信通路。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
* @author forget406
*
*/
public class Server {
private ServerSocket serverSocket;
/** 在操作系统中注册8000端口服务,并监听8000端口 */
public Server() {
try {
/* public ServerSocket(int port, int backlog)
* port表示端口号,backlog表示最多支持连接数 */
serverSocket = new ServerSocket(8000, 3);
} catch (IOException e) {
e.printStackTrace();
}
}
/** 与客户端单向交互 */
public void start() {
System.out.println("等待用户链接...");
try {
/* 创建Socket对象: public Socket accept()
* 等待客户端链接,直到客户端链接到此端口 */
Socket socket = serverSocket.accept();
System.out.println("用户链接成功,开始通讯!");
/* 服务器开始与客户端通讯 */
while(true) {
// 开启服务器socket端口到服务器内存的网路输入字节流
InputStream is
= socket.getInputStream();
// 在服务器内存中将网络字节流转换成字符流
InputStreamReader isr
= new InputStreamReader(
is, "UTF-8"
);
// 包装成按行读取字符流
BufferedReader br
= new BufferedReader(isr);
/* 中途网络可能断开
* 1)Windows的readLine会直接抛出异常
* 2)Linux的readLine则会返回null*/
String msg = null;
if((msg = br.readLine()) != null) {
System.out.println("客户端说:" +
msg
);
}
}
} catch (IOException e) {
System.out.println("链接失败");
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
===========================================
package day05;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
/**
* 客户端
* @author forget406
*
*/
public class Client {
private Socket socket;
/** 申请与服务器端口连接 */
public Client() {
try {
/* 请求与服务器端口建立连接
* 并申请服务器8000端口的服务*/
socket = new Socket("localhost", 8000);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/** 与服务器单向交互 */
public void start() {
try {
// 开启客户端内存到客户端socket端口的网络输出流
OutputStream os
= socket.getOutputStream();
// 将客户端网络输出字节流包装成网络字符流
OutputStreamWriter osw
= new OutputStreamWriter(os, "UTF-8");
// 将输出字符流包装成字符打印流
PrintWriter pw
= new PrintWriter(osw, true);
// 来自键盘的标准输入字节流
Scanner sc = new Scanner(System.in);
while(true) {
// 打印来自键盘的字符串(字节数组)
pw.println(sc.nextLine());
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
2)客户端与服务器端双向通信:客户端与服务器交互,能够实现服务器对客户端的应答,这更像是P2P模式。此时,双方socket端口均绑定来回一对通信通路。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* 服务器端
* @author forget406
*
*/
public class Server {
private ServerSocket serverSocket;
/** 在操作系统中注册8000端口服务,并监听8000端口 */
public Server() {
try {
/* public ServerSocket(int port, int backlog)
* port表示端口号,backlog表示最多支持连接数 */
serverSocket = new ServerSocket(8000, 3);
} catch (IOException e) {
e.printStackTrace();
}
}
/** 与客户端单向交互 */
@SuppressWarnings("resource")
public void start() {
System.out.println("等待用户链接...");
try {
/* 创建Socket对象: public Socket accept()
* 等待客户端链接,直到客户端链接到此端口 */
Socket socket = serverSocket.accept();
System.out.println("用户链接成功,开始通讯!");
/* 服务器接收客户端数据 */
InputStreamReader isr
= new InputStreamReader(
socket.getInputStream(),
"UTF-8"
);
BufferedReader br
= new BufferedReader(isr);
String msgReceive = null;
String msgSend = null;
/* 服务器向客户端发送数据 */
OutputStreamWriter osw
= new OutputStreamWriter(
socket.getOutputStream(),
"UTF-8"
);
PrintWriter pw
= new PrintWriter(osw, true);
Scanner sc = new Scanner(System.in);
while(true) {
if((msgReceive = br.readLine()) != null) {
System.out.println("客户端说:" + msgReceive);
}
if((msgSend = sc.nextLine()) != null) {
pw.println(msgSend);
}
}
} catch (IOException e) {
System.out.println("链接失败");
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
============================================
package day05;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
/**
* 客户端
* @author forget406
*
*/
public class Client {
private Socket socket;
/** 申请与服务器端口连接 */
public Client() {
try {
/* 请求与服务器端口建立连接
* 并申请服务器8000端口的服务*/
socket = new Socket("localhost", 8000);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/** 与服务器单向交互 */
@SuppressWarnings("resource")
public void start() {
try {
/* 客户端向服务器发送数据 */
OutputStreamWriter osw
= new OutputStreamWriter(
socket.getOutputStream(),
"UTF-8"
);
PrintWriter pw
= new PrintWriter(osw, true);
Scanner sc = new Scanner(System.in);
/* 客户端接收服务器数据 */
InputStreamReader isr
= new InputStreamReader(
socket.getInputStream(),
"UTF-8"
);
BufferedReader br
= new BufferedReader(isr);
String msgReceive = null;
String msgSend = null;
while(true) {
if((msgSend = sc.nextLine()) != null) {
pw.println(msgSend);
}
if((msgReceive = br.readLine()) != null) {
System.out.println("服务器说:" + msgReceive);
}
}
} catch (IOException e) {
System.out.println("链接失败!");
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
I/O流模式的选取原则:
1. 选择合适的节点流。在Socket网络编程中,节点流分别是socket.getInputStream和socket.getOutputStream,均为字节流。
1.1)选择合适方向的流。输入流socket.getInputStream、InputStreamReader、BufferedReader;输出流socket.getOutputStream、OutputStreamWriter、PrintWriter。
1.2)选择字节流和字符流。网络通信在实际通信线路中传递的是比特流(字节流);而字符流只会出现在计算机内存中。
2. 选择合适的包装流。在选择I/O流时,节点流是必须的,而包装流则是可选的;节点流类型只能存在一种,而包装流则能存在多种(注意区分:是一种或一对,而不是一个)。
2.1)选择符合功能要求的流。如果需要读写格式化数据,选择DataInputStream/DataOutputStream;而BufferedReader/BufferedWriter则提供缓冲区功能,能够提高格式化读写的效率。
2.2)选择合适方向的包装流。基本与节点流一致。当选择了多个包装流后,可以使用流之间的多层嵌套功能,不过流的嵌套在物理实现上是组合关系,因此彼此之间没有顺序。