网络编程中的概念
网络编程:网络上的主机,在不同进程中以编程的形式实现数据传输,实现网络通信。同一台主机中,不同进程之间也可以实现网络通信。
发送端:一次网络通信中发送数据的进程,发送端主机称为网络通信的源主机;
接收端:接收数据的进程,接收端主机称为目的主机
收发端:接收端和发送端的两端的简称。
请求和响应:获取一次网络资源进行的两次数据传输。
客户端:发送请求,获取服务的进程。
服务端:响应请求,提供服务的进程
socket(套接字)
socket是操作系统提供用于的网络编程API,基于网络通信的基本操作单元。不同操作系统提供的socket api 不一样,java对这些系统api进行了统一封装。
分类:socket套接字主要针对传输层协议划分为三类,原始套接字不关注,只关注流套接字(使用传输层TCP协议) 和 数据报套接字(使用传输层UDP协议)。
协议特点
UDP协议特点:无连接,不可靠传输,面向数据报,全双工。
TCP协议特点:可靠传输,有连接,面向字节流,全双工。
连接:通信双方需要保留对方的信息(ip地址,端口号等信息),才能进行正常通信
无连接:通信双方不需要提前保存对方信息
可靠传输:数据包不会丢失,丢失了再重新传输丢失的数据包,确保对方收到
不可靠传输:传输过程数据包可能丢失,发送数据后不关心对方是否接收到数据
面向字节流:文件操作就是字节流,读写字节可以非常灵活,10个字节可以读10次,10次可以读100个字节。TCP套接字跟文件操作有相同特点。
面向数据报:传输数据的基本单位是一个UDP数据报,读写只能操作一个完整的数据报,不能读半个数据报。
全双工:一条链路可以进行双向通信
半双工:一条链路只能进行单向通信
数据报套接字:使用传输层UDP协议
面向数据报:以数据报的形式传输接收数据,一个数据报最大为64KB字节。
DatagramSocket
对操作系统socket api的封装。系统socket api可以看做一个文件,是“网卡”这种硬件设备的抽象表现形式。对socket文件的读写操作就是间接对“网卡”进行读写操作。
普通文件是硬盘的抽象表现形式,对文件的读写就是在间接对硬盘的读写。文件就相当于操作硬件的遥控器,具有这种“遥控器”属性的在计算机中称handle “句柄”。
构造方法
DatagramSocket () //创建UDP数据报套接字的Socket,一般用于客户端(操作系统分配空闲端口);
DatagramSocket(int port) //一般用于服务端(指定端口,客户端才能知道往主机的哪个程序发送请求)
成员方法
void recive(DatagramPacket p);//接收数据(如果没有数据报传输过来就阻塞等待);
void send(DatagramPacket p) ; //发送数据报p(不会阻塞等待,直接发送)
void close() ; //相当于关闭文件,关闭此数据报套接字
DatagramPacket
DatagramPacket是对UDP数据报的抽象表示,一个DatagramPacket对象就是一个UDP数据报 ,数据传输的基本单位。
构造方法
DatagramPacket(byte[ ] buf,int length); //构造数据报来接收数据,接收的数据存在字节数组中,接收长度0到length;
DatagramPacket(byte[ ] buf ,int offset,int length,SocketAddres address); //构造数据报发送的数据报,数据为字节数组中,offset到length的数据,address为目的主机的ip和port
成员方法
InetAddress getAddress();//从数据报中获取IP地址
int getPort() ; //从数据报中获取端口号
getSocketAddress();//从socket中获取客户端地址和IP
byte[ ] getData();// 获取数据报中的数据
代码实现
在回显服务器中用数据报套接字进行网络通信
package socket;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
public class UdpEchoService {
//回显服务器:请求是啥就响应啥
DatagramSocket socket=null;
public UdpEchoService(int port) throws SocketException {
//服务器端口号一开始就得指定,否则客户端不知道往哪里发送请求,客户端端口号由系统自动分配空闲端口号
socket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {//用while一直响应客户端的请求
//读取请求并解析
DatagramPacket request = new DatagramPacket(new byte[4000], 4000);//一个对象就接收一个数据报
socket.receive(request);//输出型函数:接收数据报,然后将数据填到request的数组空间里。如果没有接收到请求,就IO阻塞
String s=new String(request.getData(),0, request.getLength());//为了方便处理数据报的数据,转成字符串
//根据请求生成响应
String r= response(s);
//将响应写回客户端
DatagramPacket socketResponse=new DatagramPacket(s.getBytes(),s.getBytes().length,request.getSocketAddress());//从数据报中解析客户端ip,port
socket.send(socketResponse);//发送数据报
}
}
private String response(String s) {
return s;
}
public static void main(String[] args) throws IOException {
UdpEchoService service=new UdpEchoService(2323);
service.start();
}
}
客户端
package socket;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket;
private String IP;
private int port;
public UdpEchoClient(String IP,int port) throws SocketException {
this.IP=IP;
this.port=port;
socket=new DatagramSocket();
}
public void start() throws IOException {
//接收控制台请求
Scanner in=new Scanner(System.in);
while (true) {
System.out.println("请输入请求");
String s = in.next();
//读取请求发送数据报
//InetAddress.getByName(IP)将字符串的IP转换成Java能识别的格式转换成32位二进制
DatagramPacket request = new DatagramPacket(s.getBytes(), 0, s.getBytes().length, InetAddress.getByName(IP), port);
socket.send(request);//socket负责接收发送,数据端口地址在数据报里
//接收服务器响应
DatagramPacket response = new DatagramPacket(new byte[4000], 4000);//创建数据报,用字节数组接收服务器发送过来的数据报里的数据
socket.receive(response);//服务器没有响应就阻塞等待
//显示响应到控制台
String r = new String(response.getData());
System.out.println(r);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",2323);
client.start();
}
}
流式套接字:使用传输层TCP协议
面向字节流:传输数据基于IO流,在IO流没有关闭的情况下,每次传输数据和接收数据没有数量限制,数据可以多次发送,也可以分开多次接收。
ServerSocket
对操作系统socket api封装的api,服务器使用 的套接字
ServerSocket(int port)//创建服务端socket并指定端口号
Socket accept();//监听创建serversocket时绑定的端口,有客户端连接后返回一个服务端socket对象, 基于此socket与客户端连接,否则阻塞等待
void close()//关闭套接字
Socket
可以存在客户端,也可以存在服务端的套接字。
构造方法
Socket(String ip,int port);//创建客户端socket,并与指定IP的主机进程建立连接
成员方法
InetAddress getInetAddress(); //返回此套接字所连接的地址
InputStream getInputStream();//获取此socket的输入流
OutputStream getOutputStream();//获取此套接字的输出流
服务器ServerSocket
1.监听来自客户端的连接请求;
2.绑定到服务器指定端口号并监听该端口的连接请求;
3.当有新的连接请求到达,接受连接并创建一个新的Socket来处理与该客户端的通信,原来的serversocket继续监听新连接;
客户端Socket
1.主动发起连接请求到指定主机进程建立连接;
2.使用socket与服务器进行数据交换;
代码实现
回显服务器用字节流套接字网络通信
package socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
ServerSocket serverSocket;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//接收请求并解析
//根据请求生成响应
//发送响应
public void start() throws IOException {
System.out.println("服务器启动");
//创建自动扩容的线程池
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
//不停接收客户端socket
//如果和客户端建立TCP连接,没有数据传输时,accept就阻塞
Socket clientSocket = serverSocket.accept();//接收客户端socket创建新的socket返回,serverSocket继续监听客户端连接
pool.submit(new Runnable() {//提交任务到线程池
@Override
public void run() {
try {
processConnection(clientSocket);//每个线程跟一个客户端建立tcp连接
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
public void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
try (InputStream inputStream = clientSocket.getInputStream();//try完了之后自动关闭输入输出流
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner scan=new Scanner(inputStream);
while (true) {
//如果客户端tcp连接没有断,当没有数据接收时hasNext阻塞,并返回false
//接收到数据返回true
//如果tcp连接断开,haseNext唤醒返回ture
if(!scan.hasNext()){
System.out.printf("[%S:%d]客户端下线",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
//接收请求并解析
String request=scan.next();
//根据请求生成响应
String response = process(request);
//将响应写回客户端
outputStream.write(response.getBytes());
//将响应打印到控制台
System.out.printf("[%s:%d],req:%s,resp:%s",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
clientSocket.close();//关闭客户端套接字,服务器进程要接收很多客户端请求
// 进行响应,不会随便销毁服务器进程来关闭所有套接字,所以要手动关闭断开连接的客户端socket
}
}
private String process(String request) {
return request + "\n";
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(1000);
server.start();
}
}
客户端
package socket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
// 客户端 Socket
private Socket socket = null;
//指定服务器的ip ,port
public TcpEchoClient(String serverIp, int serverPort) throws IOException {
// 这样的构造过程, 就会和服务器之间, 建立 tcp 连接.
// 具体建立连接的流程, 都是系统内核完成的.
socket = new Socket(serverIp, serverPort);
}
public void start() {
System.out.println("客户端启动!");
Scanner scan = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner resp = new Scanner(inputStream);
while (true) {
// 1. 从控制台读取数据
System.out.print("请输入要发送的数据: ");
String request = scan.next();//没有请求时IO阻塞
// 2. 把请求发送给服务器, 发送的请求要带有 \n, 和服务器的 scanner.next 是对应的.
// 由于上述通过 next 读到的 request 本身已经没有 \n 结尾了. 需要手动添加上换行
request += "\n";
outputStream.write(request.getBytes());
// 3. 从服务器读取到响应
if (!resp.hasNext()) {//发送后等待响应,如果tcp连接断开,退出循环
break;
}
String response = resp.next();
// 4. 把响应显示到控制台上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 1000);
client.start();
}
}