网络编程是指网络上的主机,通过不同的进程,以代码编程的方式实现网络通信(网络数据传输)
当然我们只需要满足进程不同就行,所以即便是同一个主机,只要是不同进程,基于网络来传输数据也属于网络编程。
操作系统把网络编程的一些相关操作封装起来,提供了一组API供程序员来调用 – Socket(套接字)API
发送端和接收端
在一次网络数据传输时:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念
请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送
第二次:响应数据的发送。
客户端和服务器
服务器:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
常见的客户端服务器模型
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程
Socket套接字针对传输层协议主要分为两种:流套接字,数据报套接字
流套接字:使用传输层TCP协议
TCP :传输控制协议
特点
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情
况下,是无边界的数据,可以多次发送,也可以分开多次接收。
数据报套接字:使用传输层UDP协议
UDP:用户数据报协议
特点:
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
理解有连接和无连接
有连接类似于打电话,我们和朋友打电话时,双方能听到声音并说话的前提是我电话拨通了,他也接电话了,我俩之间建立连接了,才能说话。
无连接类似于发微信,给朋友发微信不需要建立连接,只需要给他发出去就行了,至于对方什么时候看到就不确定了。
理解可靠传输和不可靠传输
可靠传输和不可靠传输并不是说发送的数据就一定能被对方收到,
可靠传输指的是发送方能知道数据是否被对方接收到了
不可靠传输指的是发送方不能知道数据是否被对方收到了
面向字节流和面向数据报
面向字节流:如果要发送100个字节,可以一次发1个字节,发送100次;也可以一次发10个字节,发送10次。可以非常灵活的完成这里的发送,接收也是如此,发送和接收的最小单位是字节
面向数据报:以一个一个的数据报为基本单位,数据报多大不确定,不同的协议有不同的约定。并且发送的时候,一次至少发送一个数据报,接收的时候,一次至少接收一个数据报
全双工
全双工:双向通信,A和B可以同时发送数据和接收数据
半双工:单向通信,同一时间只能有一方发送数据,要么A给B发,要么B给A发
DatagramSocket API
DatagramSocket 描述一个UDPSocket对象,用于发送和接收UDP数据报。
Java标准库中的DatagramSocket对象 就是表示一个Socket文件,socket本质上是一个文件描述符表,网络编程主要是通过网卡来实现的,读取socket文件中的数据,就是读取网卡,向socket文件中写入数据就是写入网卡,我们可以理解为socket文件就是一个遥控器,用来操作网卡,使我们实现网络编程
DatagraSocket构造方法
方法名 | 说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务器) |
DatagramSocket方法
方法名 | 说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报,如果没有接收到数据报,就会阻塞等待 |
void send(DatagramPacket p) | 从此套接字发送数据报包,不会阻塞等待,直接发送 |
void close() | 关闭此数据报套接字 |
DatagramPacket API
DatagramPacket 描述一个UDP数据报
UDP面向数据报,发送接收数据,就是以DatagramPacket对象为单位进行的
DatagramPacket 构造方法
方法名 | 说明 |
---|---|
DatagramPacket(byte[] buffer, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buffer)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[] buffer, int offset, int length,SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buffer)中,从0到指定长度(第二个参length)。address指定目的主机的IP和端口号 |
DatagramPacket 方法
方法名 | 说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获得发送端的主机IP地址,或从发送的数据报中,获得接收端的主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据 |
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建
InteSocketAddress API
使用InetSocketAddress(InetAddress addr,int port)方法创建一个Socket地址,包含IP地址和端口号
回显服务就是客户端发送什么,服务器就回应什么
服务器:
public class Server {
private DatagramSocket socket = null;
public Server(int port) throws SocketException {
this.socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
String response = process(request);
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
String log = String.format("[%s;%d],res: %s ,reps: %s",
requestPacket.getAddress().toString(),
requestPacket.getPort(),
request,response);
System.out.println(log);
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
Server server = new Server(9050);
server.start();
}
}
客户端
public class Client {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
public Client(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("->");
String request = scanner.nextLine();
if(request.equals("exit")){
System.out.println("exit");
return;
}
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
String log = String.format("res: %s ; reps: %s",request,response);
System.out.println(log);
}
}
public static void main(String[] args) throws IOException {
Client client = new Client("127.0.0.1",9050);
client.start();
}
}
具体分析
服务器中最核心的部分就是process方法,根据请求计算响应,此处我们实现的是回显服务,下面我们做一个简单的英译汉服务器
public class DictServer {
private DatagramSocket socket = null;
private HashMap<String,String> map = new HashMap<>();
public DictServer(int port) throws SocketException {
socket = new DatagramSocket(port);
//对hashmap进行初始化构造,可以构造多个数据
map.put("cat","猫");
map.put("dog","狗");
map.put("hello","你好");
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
String response = process(request);
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
String log = String.format("[%s,%d] req: %s ; resp: %s",
requestPacket.getAddress().toString(),
requestPacket.getPort(),
request,response);
System.out.println(log);
}
}
private String process(String request) {
//根据用户请求计算响应,英译汉的核心就是查表,构造方法中在map中加入了3个数据
//此处使用getOrDefault,key存在就返回对应的value。不存在就返回"我不会"
return map.getOrDefault(request,"我不会");
}
public static void main(String[] args) throws IOException {
DictServer server = new DictServer(9090);
server.start();
}
}
除了在process中处理业务的逻辑和回显服务器不同之外,其他部分的代码都基本相同
ServerSocket API
ServerSocket 是创建TCP服务端Socket的API
ServerSocket构造方法:ServerSocket(int port),创建一个服务端流套接字Socket,并绑定到指定端口
ServerSocket方法
方法名 | 说明 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该对Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket构造方法:Socket(String hose,int port)创建一个客户端流套接字Socket,并与对应IP的主机上对应端口的进程建立连接,也就是与想要交互的服务器建立连接
Socket 方法
方法名 | 说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
服务器
public class Server {
private ServerSocket listenSocket = null;
public Server(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
Socket clientSocket = listenSocket.accept();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) throws IOException {
String log = String.format("[Ip:%s,Port:%d] 客户端上线",clientSocket.getInetAddress(),
clientSocket.getPort());
System.out.println(log);
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
while(true){
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNextLine()){
log = String.format("[Ip:%s,Port:%d] 客户端下线",clientSocket.getInetAddress(),
clientSocket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
String response = process(request);
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
writer.flush();
log = String.format("[%s,%d] 请求: %s ; 响应: %s",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
System.out.println(log);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
Server server = new Server(9090);
server.start();
}
}
客户端
public class Client {
private Socket socket = null;
private String serverIp;
private int serverPort;
public Client(String serverIp, int serverPort) throws IOException {
this.socket = new Socket(serverIp,serverPort);
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start(){
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
while(true){
System.out.println("->");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("退出客户端");
break;
}
PrintWriter writer = new PrintWriter(outputStream);
writer.println(request);
writer.flush();
Scanner responseScanner = new Scanner(inputStream);
String response = responseScanner.next();
String log = String.format("[请求 %s : 响应 : %s]",request,response);
System.out.println(log);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Client client = new Client("127.0.0.1",9090);
client.start();
}
}
具体分析
当有客户端连接时,accetp返回一个Socket对象,得到clinetSocket,并进入processConnection方法,在这个方法中,代码会在Scanner.hasNext()位置阻塞,等待客户端发送请求,客户端发送请求后,Scanner.hasNext()结束阻塞,读取到数据后,进行下面计算响应,返回响应的操作,这一系列操作完成后,又会到Scanner.hasNext()这里阻塞,直到这个客户端退出连接,循环才结束
所以此时,这个服务器同一时间只能与一个客户端建立连接,当有多个客户端同时想要与服务器建立连接时,只能与先请求建立连接的那个客户端建立连接,等到第一个客户端推出连接后,第二个客户端才能与服务器建立连接,发送请求。
解决方案:使用多线程
主线程里面循环调用accept,每次获取到一个连接,就创建一个线程,让这个子线程来处理这个连接
public class TcpThreadEchoServer {
private ServerSocket listenSocket = null;
public TcpThreadEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
//采用多线程的方式,能够保证accept调用完毕之后能后再次立刻调用accept 每个线程对应一个客户端
//主线程循环调用accept,客户端连接后创建一个新线程来处理连接,
Socket clientScoket = listenSocket.accept();
//创建线程来给客户端提供服务
Thread t = new Thread(){
@Override
public void run() {
try {
processConnection(clientScoket);
} catch (IOException e) {
e.printStackTrace();
}
}
};
t.start();
}
}
private void processConnection(Socket clientScoket) throws IOException {
String log = String.format("[%s,%d]客户端上线",clientScoket.getInetAddress().toString(),
clientScoket.getPort());
System.out.println(log);
try (InputStream inputStream = clientScoket.getInputStream();
OutputStream outputStream = clientScoket.getOutputStream()){
while (true){
//1.读取请求并解析
Scanner scanner = new Scanner(inputStream);
if(!scanner.hasNextLine()){
log = String.format("[%s,%d]客户端下线",clientScoket.getInetAddress().toString(),
clientScoket.getPort());
System.out.println(log);
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.将响应返回给客户端
PrintWriter writer = new PrintWriter(outputStream);
writer.println(response);
writer.flush();
log = String.format("[%s:%d] res %s ; resp %s",
clientScoket.getInetAddress().toString(),
clientScoket.getPort(),request,response);
System.out.println(log);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
clientScoket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
server.start();
}
}
总体来看,代码和之前的回显服务器的代码差别不大,只是在处理连接时,加入了多线程。
但是如果有很多的客户端连接又退出,就会导致服务器频繁的创建和销毁线程,这时就需要很大的成本,所以我们直接引入线程池,
public class TcpThreadPoolServer {
private ServerSocket listenSocket = null;
public TcpThreadPoolServer(int port) throws IOException {
this.listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
//创建线程池,将处理连接这个任务循环放入到线程池中
ExecutorService executorService = Executors.newCachedThreadPool();
while (true){
Socket clientSocket = listenSocket.accept();
//使用线程池处理连接
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
//其他代码一样,只需要在start方法中加入线程池
基于上面改进后的TCP服务器,实现一个翻译服务器。
写了几次的服务器后,我们发现翻译服务器的代码和回显服务器的代码基本差不多,只有process不一样,所以我们可以通过继承的方式来实现代码复用,只需要重写process方法,重新实现 根据请求计算响应的逻辑 就可以了
public class TcpDictServer extends TcpThreadPoolServer{
private HashMap<String,String> map = new HashMap<>();
public TcpDictServer(int port) throws IOException {
super(port);
map.put("hello","你好");
map.put("cat","猫");
map.put("dog","狗");
}
@Override
public String process(String request){
return map.getOrDefault(request,"我不会");
}
public static void main(String[] args) throws IOException {
TcpDictServer server = new TcpDictServer(9090);
server.start();
}
}