网络上的主机通过不同的进程,以编程的方式实现网络通信,我们称之为网络编程。
我们只要满足不同的进程即可,所以即便是同一个主机,只要是不同的进程,基于网络传输数据,也是属于网络编程
在一次网络数据传输时,有发送端、接收端、收发端三方
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送;
第二次:响应数据的发送;
服务端:提供服务的一方进程,成为服务端,可以提供对外服务;
客户端:获取服务的一方进程,成为客户端;
对于服务来说,一般提供1、客户端获取服务资源。2、客户端保存资源在服务端
由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。
基于Socket套接字的网络程序开发就是网络编程。
Socket套接字主要针对传输层协议,分为三类:
1、流套接字:使用传输层TCP协议
TCP的特点:有连接、可靠传输、面向字节流,全双工
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情
况下,是无边界的数据,可以多次发送,也可以分开多次接收。
2、数据报套接字:使用传输层UDP协议
UDP的特点:无连接、不可靠传输、面向数据报,全双工
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一
次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
3、原始套接字
这就是一次发送端UDP数据报发送,即接收端的数据报接收,并没有返回的数据,也就是只有请求,没有响应;
DatagramSocket 是UDP Socket,用于接收和发送数据报;
认识一下DatagramSocket 的构造方法
普通方法
DatagramPacket是UDP Socket发送和接收的数据报;
即socket.receive()的参数都是DatagramPacket类型的;
认识一下:DatagramPacket 构造方法:
普通方法
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
注意:
每次接收(receive)、发送数据(send)都是在传输一个DatagramPacket 对象
InetSocketAddress ( SocketAddress 的子类 )构造方法:
服务端部分:
public class UdpEchoSever {
DatagramSocket socket = null;
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务端!");
//Udp是不需要连接的,直接接收数据即可
while(true){
//第一步:读取客户端发来的需求
//需求必须是由DatagramPacket类型的容器来放,由receive读取并填充到DatagramPacket对象中
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);
//请求一般是String的,所以把DatagramPacket解析成String
String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
// 得到的长度不一定是1024,实际数据可能不够1024
//第二步:根据请求计算响应
String response = process(request);
//第三步:把响应写会客户端
DatagramPacket reponsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(reponsePacket);
System.out.printf("[%s:%d] req: %s,resp:%s\n",reponsePacket.getAddress().toString(),responsePacket.getPort(),
request,response);
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoSever sever = new UdpEchoSever(9090);
sever.start();
}
}
public class UdpEchoClient {
/**
* 五元组
* 1、源ip
* 2.源端口
* 3、目的ip
* 4、目的端口
* 5.协议类型(UDP/TCP)
*/
DatagramSocket socket = null;
private String severIP;
private int severPort;
public UdpEchoClient(String ip, int port) throws SocketException {
//此处的ip和port都是服务端的
socket = new DatagramSocket();
severIP = ip;
severPort = port;
}
public UdpEchoClient() throws SocketException {
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner sc = new Scanner(System.in);
while(true){
//1、从控制台得到用户输入的字符串
System.out.print("-> ");
String request = sc.nextLine();
//2、把这个字符串打包成一个UDP请求、并发送(明确知道服务器的IP和端口号)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(severIP),severPort);
socket.send(requestPacket);
//3、从服务器读取相应数据,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,requestPacket.getLength(),"UTF-8");
//4、打印响应数据
System.out.printf("req: %s,resp: %s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
//由于服务器和客户端在同一个机器上,所以ip仍然是127.0.0.1,如果是其他机器,ip地址就要改动
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
udpEchoClient.start();
}
}
同时启动客户端和服务端,我们就可以运行一个简单的回显服务器,但是IDEA默认设置只有一个实例,如果启动多个实例,就会销毁之前的实例,因此如果我们想启动多个实例,就要修改一下运行配置,如下图:
下图是打开三个客户端和一个服务端的情况:
简述该服务器:输入名字,如果存在该同学,就输出该同学对应的号码,否则输出“错误”;
在上面的服务器的基础上,重新写一个服务器继承上面的服务器,客户端不需要改动,看代码示例 :
public class UdpFindSever extends UdpEchoSever{
private HashMap<String,String> dict = new HashMap<>();//哈希表来存储所有同学信息
//父类只声明了有参的构造函数,所以子类必须重写父类有参的构造方法
public UdpFindSever(int port) throws SocketException {
super(port);
dict.put("王某某","111222333");
dict.put("李某某","145634788");
dict.put("赵某某","009909876");
dict.put("钱某某","475839583");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"错误!");
}
public static void main(String[] args) throws IOException {
UdpFindSever sever = new UdpFindSever(9090);
sever.start();
}
}
服务端
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpThreadEchoSever {
private ServerSocket severSocket = null;
public TcpThreadEchoSever(int port) throws IOException {
severSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while(true){
/**
* 1、TCP是有连接的,所以不能直接读取数据,要先建立连接
* 2、accept就是在建立连接,如果没有客户端建立连接,accept就会阻塞
* 3、如果有客户端建立连接,accept就会返回一个socket对象,称之为clientSocket,后续客户端和服务器的沟通就是通过clientSocket来完成的
*
*/
Socket clientSocket = severSocket.accept();
//利用多线程来改进,每有一个客户端申请连接,服务端就会有一个线程进行接待,因此可以做到一个服务端接收多个客户端
Thread thread = new Thread(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
public void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
/**
* 处理请求和响应
*/
try(InputStream inputStream = clientSocket.getInputStream()){
try(OutputStream outputStream = clientSocket.getOutputStream()) {
//循环处理每个需求
Scanner sc = new Scanner(inputStream);
while(true){
//1、读取请求
if(!sc.hasNext()){
System.out.printf("[%s:%d],客户端断开连接\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String request = sc.nextLine();
//2.根据请求,计算响应
String response = process(request);
//3.把响应返回给客户端
PrintWriter printWriter = new PrintWriter(outputStream);//把outputStream用PrintWriter包裹一下
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] req: %s,resp: %s\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort(),request,response);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
clientSocket.close();
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpThreadEchoSever tcpEchoSever = new TcpThreadEchoSever(9090);
tcpEchoSever.start();
}
}
客户端
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String severIP,int severport) throws IOException {
/**
* 这里的ip和端口号是我们要连接的服务器的iip和端口号,并不是定义的客户端的ip和端口号
*/
socket = new Socket(severIP,severport);
}
public void start(){
System.out.println("和服务端连接成功!");
Scanner sc = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream()){
try(OutputStream outputStream = socket.getOutputStream()){
while(true){
//1、从控制台读取用户需求
System.out.printf("-> ");
String request = sc.next();
if(request.equals("exit")){
System.out.println("断开连接!\n");
break;
}
//2、将独到的内容打包成请求,并发送给服务端
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();
//3、读取服务器响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4、将相应内容显示在控制台上
System.out.printf("req: %s,resp: %s\n",request,response);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}
结果:
注意:
1、因为TCP是有连接通信,所以我们要用多线程的方式来接收客户端的请求,否则一个服务端只能链接客户端,当我们用多线程的方式来接待客户端,此时一个服务端就可以调用多个线程来对应的响应多个客户端;
2、为什么UDP版本不需要使用多线程?
因为UDP是无连接通信,客户端不需要服务端确认接收也可以发送,简言之就是UDP没有TCP依赖性强;UDP就像是发信息,直接发送内容即可,不需要确认对方是否正在盯着手机看。但是TCP就像是打电话,我们拨电话之后,要等到对方接电话才可以说谈话内容,如果对方不接电话,我们就没法进行沟通。在TCP版本的情况下运用多线程,就是我们拨电话之后,对方派A和我们沟通,又来一个电话,对方派B来沟通。