网络编程
网络编程的基本概念
什么是网络编程
在生活中是离不开网络的,我们经常从网络上获取一些资源(像通过浏览器浏览一些视频网站,从里面获取一些视频资源),这个过程都是通过网络编程来实现进行数据传输
网络编程 就是网络上的主机,通过不同的进程 ,进行网络传输数据(以网络编程实现的网络通信 )
只要是不同进程就行,因此即使是同一个主机,不同进程也可以进行网络编程

进程A:获取网络资源
进程B:提供网络资源
基本概念
1.接收端和发送端
发送端 :数据的发送方进程,称为发送端,此主机可以称为源主机
接收端 :数据的接收方进程,称为接收端,此主机可以称为目的主机
收发端 :发送端和接收端两端

- 请求和响应
当进程A可以请求数据发送 ,进程B就响应数据的发送
3.客户端和服务器
客户端 :获取服务一方的进程
服务器:提供服务一方的进程,可以对外提供服务

Socket套接字
概念
Socket套接字,是系统提供用于网络通信的技术,基于这个Socket编程网络程序开放就是网络编程
分类
1.流套接字 :TCP协议,即Transmission Control Protocol(传输控制协议)
特点:有连接、可靠传输、面向字节流、全双工、有接收和发送缓冲区、大小无限
2.数据报套接字 :UDP协议,User Datagram Protocol(用户数据报协议)
特点:无连接、不可靠传输、面向数据报、全双工、有接收缓冲区无发送缓冲区、大小受限一次最多传输64KB
3.原始套接字:自定义传输层协议,用于读写内核没有处理的IP协议数据
1.有连接/无连接 :通信双方是否互相保存对方的核心信息(IP和端口号)
TCP就保存了,在结束的时候就释放了,UDP没有保存 ,但是传输信息中也是有相关信息的
2.可靠传输/不可以靠传输 :可靠传输就是丢包的概率 ,像TCP可靠传输,其丢包的概率比较低,UDP不可靠传输,不关心丢不丢包
3.面向字节流和数据报 :面向字节流,读写文件比较方便可以使用InputStream和OutputStream ;面向数据报:每次读取只可以以一个UDP为单位进行
4.全双工和半双工:全双工:可以双向传输,半双工:只可以单向传输


UDP数据报套接字编程
Java提供的一些API介绍
DatagramSocket
| 方法 | 说明 |
|---|---|
| DatagramSocket() | 创建一个UDP数据报套接字Socket,绑定本机任意一个端口号 |
| DatagramSocket(int port) | 创建一个UDP数据报套接字Socket,绑定到指定port端口号 |
端口号0~65535,但是 0 - 1023其有特殊含义不可以选择
这里一般情况下,不带参数的都用于客户端,一个服务器可能对应多个客户端,但是这些端口号不可以相同 ,如果我们自己手动创建可能会出现相同,因此让操作系统任意分配,这样可以有效防止端口号相同从而导致的冲突,手动指定端口号,带参数的构造方法一般由于服务器
| 方法 | 说明 |
|---|---|
| void reveive(DatagramPacket p) | 从p套接字接收数据报 |
| void send(DatagramPacket p) | 从套接字发送数据报 |
| void close() | 关闭数据报套接字 |
这里的receive方法,如果没有接收到数据报,其就会进行阻塞等待
而send不会阻塞等待,直接发送
DatagramPacket
| 方法 | 说明 |
|---|---|
| DatagramPacket(byte buf[], int offset, int length) | 构造一个DatagramPacket对象来接收数据报,保存在buf数组中,接收长度为length |
| DatagramPacket(byte buf[], int offset, int length, SocketAddress address) | 同理,address是目的主机的IP和端口号 |
| 方法 | 说明 |
|---|---|
| InetAddress getAddress() | 从接收数据报中,获取发送端的IP地址,从发送数据报中,获取接收端IP地址 |
| int getPort() | 同理,用于获取端口号 |
| byte[] getData() | 获取数据报中的数据 |
InetSocketAddress
因为上面的DatagramPacket有一个参数是SocketAddress,这里的这个是其子类
| 方法 | 说明 |
|---|---|
| InetSocketAddress(InetAddress addr, int port) | 创建一个socket地址,包含地址和端口号 |
回显实例
服务器
1.此时的构造函数,根据自己提供的端口号进行实例化DatagramSocket 对象

2.启动过程
获取客户端请求

先创建一个DatagramPacket用于存放请求,并且要先申请好空间
并且使用receive接收请求,此方法中的参数是输出型参数,请求的内容会放入到requestPacket对象上
计算响应此处是回显服务器,不做任何处理,将请求方发送信息发回去即可
将请求的结果返回给客户端此处要将计算响应返回的字符串,转换成DatagramPacket返回给服务器

并且这里不仅要将数据放入进去,也要将发送请求的客户端对应的IP和端口号放入进去,这样才可以找到客户端
java
//udp回显服务器
public class UdpEchoServer {
private DatagramSocket socket = null;
//根据一定的端口号进行创建服务器
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("server start");
while (true){
//1.先获取请求,此时requestPacket是一个输出型参数
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);
//将二进制转换成字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.计算响应(这里是回显服务器,这里没有计算过程)
String response = process(request);
//3.将请求返回给客户端,并且这里要传入客户端的地址
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印日志
System.out.printf("[%s : %d],request:%s,response : %s",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
System.out.println();
}
//此处的计算响应直接返回即可
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
//这里的端口号不可以重复
UdpEchoServer server = new UdpEchoServer(9900);
server.start();
}
}
客户端
构造方法 ,这里客户端并不需要指定端口号,让系统自动分配即可,因为这里客户端比较多,出现重复概率较高,让系统自动分配恰好解决了这一痛点
虽然我们不需要指定端口号,但是这个我们要确定客户端的IP和端口号,因为这样服务器进行接收数据时候才可以通过客户端的IP和端口号,将数据返回给客户端

启动客户端
1.先进行输入

2.将输入字符串变成一个DatagramPacket对象传给服务器 ,虽然UDP并不需要保存对方的核心信息(IP和端口号),但是也要知道这些信息 才可以找到对应的IP和端口号


3.接收服务器返回的数据
依旧使用一个DatagramPacket对象进行接收,并且这里receive参数是输出型参数

java
//客户端
public class UdpEchoClient {
//创建socket对象
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
private UdpEchoClient(String serverIp,int serverPort) throws SocketException {
socket = new DatagramSocket();//客户端不需要指定端口号,系统自动分配
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("client start");
Scanner scanner = new Scanner(System.in);
//通过客户端将这个发送给服务器
while (true){
//1.用户进行输入
System.out.print("->");
String request = scanner.next();
if(request.equals("exit")){
break;
}
//2.将用户写入字符串构造成UDP数据报,发送给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length,
InetAddress.getByName(this.serverIp),//将这个Ip转换成InetAddress
this.serverPort);
socket.send(requestPacket);
//3.从服务器中读取响应
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);//输出型参数
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9900);
client.start();
}
}
客户端和服务器结合使用

exit结束客户端

这里可以一个服务器对应多个客户端

服务器

三个不同的客户端



这里可以将这个服务器修改一下,让其变成一个查询字典的功能
此时只需要一个新的类来继承上面的UdpEchoServer这个服务器即可 ,我们这里只有处理过程变了而已,所以继承重写一下process方法即可
java
public class UdpDictServer extends UdpEchoServer{
private Map<String ,String> map = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
//这里直接将对应关系建立起来即可
map.put("hello","你好");
map.put("world", "世界");
map.put("cat", "小猫");
map.put("dog", "小狗");
map.put("pig", "小猪");
map.put("like","喜欢");
}
@Override
public String process(String request) {
return map.getOrDefault(request,"没有找到这个单词");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9900);
server.start();
}
}
直接使用一个哈希表,将其对应关系单词和中文对应关系即可


TCP流套接字编程
ServerSocket
这个是创建TCP服务器端Socket的API
构造方法
| 方法 | 说明 |
|---|---|
| ServerSocket(int port) | 创建一个服务端流套接字Socket,绑定port端口号 |
| 方法 | 说明 |
|---|---|
| Socket accept() | 接收客户端连接,有客户端连接就,返回一个Socket对象,基于这个与客户端进行连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
因为其创建对象也会占用空间,因此结束所有操作,要将这个数据给释放,否则会导致内存泄漏
Socket
Socket是客户端Socket,其是在双方进行联系的时候,保存对端的信息及其接受发数据
| 方法 | 说明 |
|---|---|
| Socket(String host,int port) | 创建一个客户端流套接字,与对应IP和端口号进建立连接 |
方法
| 方法 | 说明 |
|---|---|
| InetAddress getInetAddress() | 返回所连接的地址 |
| InputStream getInputStream() | 返回此套接字输入流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
回显实例
TCP服务器
1.构造函数同理使用,需要自己手动指定端口号

启动服务器
1.这里需要使用Socket流套接字对象来接收其请求

2.计算响应(这里是回显服务器,所有直接将其接收的数据返回即可)
3.返回

这里的ServerSocket负责接收数据,但是处理数据是通过Socket
这里ServerSocket就像酒店前台,在前台开了房间,而Socket就像酒店服务员将你带到你所在的房间
这里Socket对象是一个文件,因此可以使用这些关于文件的操作,进行读取和写入
java
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//如果线程较多的话,其频繁的创建和销毁也是比较浪费时间的,因此可以使用线程池
ExecutorService executorService = Executors.newCachedThreadPool();
public void start() throws IOException {
System.out.println("server start");
while (true){
//使用accept进行连接,并且会返回Socket对象
Socket socket = serverSocket.accept();
//通过这个方法进入处理连接过程,并且一旦调用就不会调用accept
processConnection(socket);
//因为这里会进行等待,导致这里只可以有一个客户端进行操作,因此这里可以使用多线程
//但是使用多线程多了,其销毁和创建也需要时间
// Thread thread = new Thread(() ->{
// processConnection(socket);
// });
// thread.start();
executorService.submit(() ->{
processConnection(socket);
});
}
}
public void processConnection(Socket socket){
System.out.printf("[%s:%d服务器上线",socket.getInetAddress(),socket.getPort());
System.out.println();
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
//1.读取并进行解析
if(!scanner.hasNext()){
System.out.printf("[%s:%d服务器下线",socket.getInetAddress(),socket.getPort());
break;
}
//2.根据请求计算响应
String request = scanner.next();
String response = process(request);
//3.返回
writer.println(response);
writer.flush();
System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),
request, response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
//这里完成以后要进行释放内存空间
try {
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
//直接返回即可
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9900);
tcpEchoServer.start();
}
}
客户端
构造方法,此时这里需要指定IP和端口号

启动客户端
这里依旧使用Scanner和writer分别进行发送和读取数据

java
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//因为其是连接的,所以需要服务器Ip 和 端口
socket = new Socket(serverIp, serverPort);
//socket.connect(new InetSocketAddress((InetAddress.getByName(serverIp)),serverPort));
}
public void start(){
System.out.println("client start");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(System.in);
Scanner scannerNetwork = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
//1.从控制台输入
System.out.print("->");
String request = scanner.next();
//2.将请求发送给服务器
writer.println(request);
writer.flush();//去除缓冲区
//3.接收
String response = scannerNetwork.next();
System.out.println(response);
}
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9900);
tcpEchoClient.start();
}
}


虽然这样可以正常使用这个,但是如果换成多个客户端,这里会出现问题




这里就会出现这样的问题,只有一个服务器在操作,其他服务器都没有正常工作

引入多线程
因为上面无法连接多个客户端,因此这里我们可以将其设置成多多线程
java
public void start() throws IOException {
System.out.println("server start");
while (true){
//使用accept进行连接,并且会返回Socket对象
Socket socket = serverSocket.accept();
//通过这个方法进入处理连接过程,并且一旦调用就不会调用accept
// processConnection(socket);
//因为这里会进行等待,导致这里只可以有一个客户端进行操作,因此这里可以使用多线程
//但是使用多线程多了,其销毁和创建也需要时间
Thread thread = new Thread(() ->{
processConnection(socket);
});
thread.start();
}
}



引入线程池
但是当线程变多了,其创建和销毁也会比较浪费时间,因此这里就可以使用线程池
java
ExecutorService executorService = Executors.newCachedThreadPool();
public void start() throws IOException {
System.out.println("server start");
while (true){
//使用accept进行连接,并且会返回Socket对象
Socket socket = serverSocket.accept();
executorService.submit(() ->{
processConnection(socket);
});
其实可以进一步优化为IO多路复用,就是一个线程对应多个客户端,当某一个客户端需要处理时候,这个客户端对应的socket才会过来处理
细节处理
内存泄漏问题

在其服务器中会不断的创建socket,因此当这个对象不实用的时候,需要进行释放资源
缓冲区问题

这里的writer里面内置了缓冲区,为了避免重复的写入数据,其达到一定程度,当其缓冲区满了,其才是真正的写入,因此这里使用flush进行冲刷缓冲区,这样就不满了才是真正的写入
隐藏条件
TCP中,其请求和响应都是以\n进行结尾

当然这个也可以像UDP那样写一个查字典功能
java
public class TcpDIctServer extends TcpEchoServer{
private Map<String,String > map = new HashMap<>();
public TcpDIctServer(int port) throws IOException {
super(port);
//这里直接将对应关系建立起来即可
map.put("hello","你好");
map.put("world", "世界");
map.put("cat", "小猫");
map.put("dog", "小狗");
map.put("pig", "小猪");
}
@Override
public String process(String request) {
return map.getOrDefault(request,"没找到这个单词");
}
public static void main(String[] args) throws IOException {
TcpDIctServer tcpDIctServer = new TcpDIctServer(9900);
tcpDIctServer.start();
}
}


