很容易搞混,得好好理理
目录
1.网络编程的基本概念
1.1服务器
1.2客户端
2.Socket套接字
2.1概念
2.2分类
3.UDP数据报套接字编程
3.1DatagramPocket? API
3.2DatagramSocket? ?API
3.3UDP实现回显服务
4.TCP流套接字编程
4.1ServerSocket API
4.2Socket API
4.3TCP实现回显服务
4.3.1用多线程进行优化
4.3.2用线程池进行优化
1.网络编程的基本概念
①什么是网络编程: 网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输。
②请求和响应分别指的什么? 一般来说,获取一个网络资源,涉及到两次网络数据传输: 第一次:请求数据的发送 第二次:响应数据的发送。 举个例子: 请求和响应在生活中是很好理解的,我手机没电了向别人借一下手机就是一个请求,而别人的回应就是响应。
1.1服务器
在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端(也称服务器),可以提供对外服务。通俗可以理解成供应者。
1.2客户端
获取服务的一方进程,称为客户端。通俗可以理解成获得者。
2.Socket套接字
2.1概念
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
2.2分类
①流套接字:(使用传输层TCP协议)
a.TCP的特点:
有连接,可靠,面向字节流,全双工
b.分别解释一下以上名词:
有连接: 需要连通,才能交换数据(比如打电话,需要你接了之后才能够和你进行通话)
可靠: 传输过程中发送方知道接收方有没有接收数据(打电话,接了电话那么就会接收到数据,否则就不知道是否接收了数据)
面向字节流: 以字节为单位进行传输的方式
全双工: 一条链路双向通信(无疑,无论是打电话还是发QQ都是一种双向的操作)
②数据报套接字:(使用传输层UDP协议)
a.UDP特点:
无连接,不可靠,面向数据报,全双工
b.分别解释一下以上名词:
无连接: 不需要连接,直接就能发送数据(比如发QQ,我不需要得到你的允许就可以直接给你发送)
不可靠: 不知道传输的对象有没有接收到信息(发QQ别人未回,我们不知道是已读还是未读)
面向数据报: 以数据报为单位进行传输,一次发送/接收都必须是完整的一个数据报或者多个数据报,不能是半个啊,一个半数据报这种
全双工: 一条链路双向通信(无疑,无论是打电话还是发QQ都是一种双向的操作)
③原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的
IP
协议数据。我们不学习原始套接字,简单了解即可。
3.UDP数据报套接字编程
对客户端发送请求,服务器接收请求并作出回应的过程图(这里是一次请求一次回应)
3.1DatagramPocket? API
①什么是DatagramPocket?
DatagramPacket
是
UDP
发送和接收的数据报。每次发送/接收数据,都在传输一个DatagramPocke对象
②DatagramPocket的构造方法:
方法签名
| 方法说明 |
DatagramPacket(byte[] buf, int length)
|
构造一个
DatagramPacket
以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf
)中,接收指定长度(第二个参数为length)
|
DatagramPacket(byte[]
buf, int offset, int length,
SocketAddress address)
|
构造一个
DatagramPacket
以用来发送数据报,发送的数据为字节数组(第一个参数buf
)中,从
0
到指定长度(第二个参数length)。
address
指定目的主机的
IP
和端口号
|
注意:
构造
UDP
发送的数据报时,需要传入
SocketAddress
,该对象可以使用
InetSocketAddress
来创建。
InetSocketAddress
(
SocketAddress
的子类 )构造方法:
方法签名 | 方法说明 | InetSocketAddress(InetAddress address, int port) | 创建一个Socket地址,包含IP地址和端口号 |
③
DatagramPocket的方法:
方法签名 | 方法说明 |
InetAddress
getAddress()
|
从接收的数据报中,获取发送端主机
IP
地址;或从发送的数据报中,获取接收端主机IP
地址
|
int getPort()
|
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
|
byte[]getData()
|
获取数据报中的数据
|
3.2DatagramSocket? ?API
①什么是DatagramSocket?
创建了一个UDP版本的Socket对象,实质上代表着操作系统中一个Socket文件(更进一步而言是对网卡进行的读写的操作)
②DatagramSocket的构造方法:
方法签名 | 方法说明 |
DatagramSocket()
|
创建一个
UDP
数据报套接字的
Socket
,绑定到本机任意一个随机端口(一般用于客户端)
|
DatagramSocket(int
port)
|
创建一个
UDP
数据报套接字的
Socket
,绑定到本机指定的端口(一般用于服务端)
|
③DatagramSocket的方法:
方法签名 | 方法说明 |
void receive(DatagramPacket p)
|
从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
|
void send(DatagramPacket
p)
|
从此套接字发送数据报包(不会阻塞等待,直接发送)
|
void close()
|
关闭此数据报套接字
|
3.3UDP实现回显服务
①什么是回显服务?
请求的是啥,回应的就是啥的操作
②对回显服务进行代码实现:
相关注意事项注释中已经讲解得非常细了,这里只着重强调一个点
?客户端:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UDPClient {
//首先需要有一个DataframSocket实例,这是进行网络编程的前提
private DatagramSocket socket=null;
private String serverIP;//IP地址,这里是指服务器传过来的
private int serverPort;//端口号,这里是指服务器传过来的
public UDPClient(String Ip,int port) throws SocketException {
socket = new DatagramSocket();
//这里就是用无参版本的构造socket对象,就是让系统随机分配一个端口号
serverIP=Ip;//这个指定的是服务器的IP地址和端口号和客户端没有关系
serverPort=port;
}
public void start() throws IOException {
Scanner input= new Scanner(System.in);
while (true){
//首先先从控制台读取到用户输入的信息,然后才能发送
System.out.println("->");
String request = input.next();
//然后把用户写的信息构造成一个UDP请求,才能发送,指定IP和端口号,才知道传到了哪个服务器去
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
//然后发送UDP数据报
socket.send(requestPacket);
//发送完之后需要再接收从服务器得到的响应数据
//这里仍然需要用一个字节类数组来对返回内容进行接收
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
//然后再把响应的数据写成字符串形式,然后打印到控制台
String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
System.out.printf("req:%s,resp:%s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UDPClient udpClient=new UDPClient("127.0.0.1",3030);
udpClient.start();
}
}
服务器:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UDPServer {
//首先需要有一个DataframSocket实例,这是进行网络编程的前提
private DatagramSocket socket = null;
public UDPServer(int port) throws SocketException {
//这个地方会出现异常的原因
//1.端口号已经被占用了,同一主机的两个程序不能有相同的端口号
//2.2.每个进程能打开的文件个数是有上限的,资源耗尽后是打不开的
socket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器!");
//由于UDP的不可靠性,因此可以直接接收从客户端传来的数据,不用进行连接
while(true){
//这个时候就需要等待客户端发来请求,进行接收并作出回应
//因为服务器本身就是被动接收数据的,而不是发送
//构造一个Datagrampocket来对数据报进行发送和接收
// 且接收应该用一个空的byte类型的数组对内容来进行接收(这是DatagramPocket的一个构造方法)
DatagramPacket requestPacket=new DatagramPacket(new byte[1024],1024);
//当没有接收到客户端的请求时,receive()就会阻塞,直到接收到才停止阻塞
socket.receive(requestPacket);
//接收到客户端的请求后需要作出回应
//此时已经将接收到的数据放在requestPacket里面了,然后将这里面的数据解析出来,解析成字符串
//new String()方法可以将字符,字节转换为字符串
String request=new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");//也可以指定字符编码
//解析后需要找到一个新的字符串来进行存放,并产生相应
String response = prcoess(request);
//对客户端作出响应
//这里不仅要放回响应内容,而且要指定好要发送到的IP地址和端口号
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
//发送过来的是DatagramPacket数据报,返回的也就是这样的数据报,但是发送回去的就不再是一个空的了,而是应该把从上面所得到的响应字符串放到里面然后
socket.send(responsePacket);
//可以再手动输出一下客户端的IP地址和端口号
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
}
}
public String prcoess(String request){
return request;
}
public static void main(String[] args) throws IOException {
UDPServer udpServer=new UDPServer(3030);
udpServer.start();
}
}
③一个简单翻译程序的实现(代码):
客户端没有更改,只是把服务器进行了调整
package UDP;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UDPdictServer extends UDPServer {
private HashMap<String,String> dict=new HashMap<>();
public UDPdictServer(int port) throws SocketException {
super(port);
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("fuck", "卧槽");
dict.put("pig", "小猪");
}
@Override
public String prcoess(String request) {
return dict.getOrDefault(request, "该词无法被翻译!");
}
public static void main(String[] args) throws IOException {
UDPdictServer server = new UDPdictServer(3030);
server.start();
}
}
4.TCP流套接字编程
关于TCP的执行过程图
4.1ServerSocket API
①什么是ServerSocket?
ServerSocket 是创建TCP服务端Socket的API。
②ServerSocket的方法:
方法签名 | 方法说明 |
Socket accept()
|
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端
Socket对象,并基于该Socket
建立与客户端的连接,否则阻塞等待
|
void close()
| 关闭此套接字 |
4.2Socket API
①什么是Socket?
Socket
是客户端
Socket
,或服务端中接收到客户端建立连接(
accept
方法)的请求后,返回的服务端Socket。
②Socket的构造方法:
方法签名 | 方法说明 |
Socket(String host, int port)
|
创建一个客户端流套接字
Socket
,并与对应
IP
的主机上,对应端口的进程建立连接
|
③Socket的方法:
方法签名 | 方法说明 |
InetAddress getInetAddress()
|
返回套接字所连接的地址
|
InputStream getInputStream()
|
返回此套接字的输入流
|
OutputStream getOutputStream()
|
返回此套接字的输出流
|
4.3TCP实现回显服务
代码实现:(相关细节内容在代码注解中讲得很详细)
客户端:
package TCP;
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 TCPClient {
private Socket socket=null;
public TCPClient(String serverIP,int serverPort) throws IOException {
//对于UDP的socket和TCP的serverSocket来说,构造函数里面给定的端口号,就是指定自己要绑定哪个端口号
//而这里对于TCP的Socket来说,这里指定的IP和端口号是表示自己要与哪一个服务器进行连接,这里的IP和端口号都是服务器的
//调用这个构造方法就会和服务器进行连接,就会使accept的立马进行接通
socket = new Socket(serverIP,serverPort);
}
//启动客户端
public void start() throws IOException {
System.out.println("和服务器连接成功!");
Scanner input = new Scanner(System.in);
//因为TCP是流来传输运行的
try(InputStream inputStream=socket.getInputStream()){
try(OutputStream outputStream=socket.getOutputStream()){
while(true){//此时仍然是需要四个步骤
//1.从控制台读取字符串
System.out.println("从控制台读取到的字符串->");
//客户端要输入请求
String request=input.next();
// 2. 根据读取的字符串,将所读的字符串构造为请求, 把请求发给服务器
//用printWriter来对读取的内容进行接收
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush(); // 如果不刷新, 可能服务器无法及时看到数据.
//3.读取到服务器来的响应,并对其写入的内容进行解析
//然后从服务器中读取响应,并解析
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//打印先关结果
System.out.printf("req:%s,resp:%s\n",request,response);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPClient tcpClient=new TCPClient("127.0.0.1",9090);
tcpClient.start();
}
}
服务器:
package TCP;
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 TCPServer {
private ServerSocket serverSocket = null;
public TCPServer(int port) throws IOException {
serverSocket=new ServerSocket(port);//给服务器指定端口号
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
// 进一步讲, serverSocket 就干了一件事, 接电话
//进一步来说是建立了连接然后应答上,用clientSocket来进行后续操作
Socket clientSocket = serverSocket.accept();
//对接收的响应进行处理
processConnection(clientSocket);
}
}
public void processConnection(Socket clientSocket) throws IOException {
//IP地址和端口来进行打印
System.out.printf("[%s,%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//对客户端发来的请求进行响应处理
// 这里的针对 TCP socket 的读写就和文件读写是一致的
try(InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream=clientSocket.getOutputStream()) {
// 循环的处理每个请求, 分别返回响应
Scanner input = new Scanner(inputStream);
while(true){
//读取请求
//已经读取完,不会再输入新的内容
while(!input.hasNext()){
//返回客户端的ip地址以及端口号
System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
//对请求进行处理
String request=input.next();
// 2. 根据请求, 计算响应
String response = process(request);
// 3. 把这个响应返回给客户端
// 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
//读取返回响应所输入的内容用printWriter包裹,返回
PrintWriter printWriter = new PrintWriter(outputStream);
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) {
e.printStackTrace();
} finally {
// 要关闭,因为资源时有限的,一个服务器不止为1个客户端进行服务,用了之后要及时关闭
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TCPServer tcpServer=new TCPServer(9090);
tcpServer.start();
}
}
结果如下:
?此时我们会发现,如果是一个客户端的话,与服务器之间的连接是没有问题的,如果是多个客户端,就不能够进行连接。那这到底是为什么呢?
但是显然在我们实际中,这样是不可取的,大大浪费了服务器的资源,所以我们引入了线程池,或者多线程来解决这个问题,代码如下。
4.3.1用多线程进行优化
代码如下:(对服务器部分进行修改)
package TCP;
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 TCPServer {
private ServerSocket serverSocket = null;
public TCPServer(int port) throws IOException {
serverSocket=new ServerSocket(port);//给服务器指定端口号
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
// 进一步讲, serverSocket 就干了一件事, 接电话
//进一步来说是建立了连接然后应答上,用clientSocket来进行后续操作
Socket clientSocket = serverSocket.accept();
//使用多线程:
Thread t = new Thread(()->{
try {
processConnection(clientSocket);//这样之后每一个客户端的连接就从串行变成并行的了,然后每一个客户端的连接就可以成功了
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
}
}
public void processConnection(Socket clientSocket) throws IOException {
//IP地址和端口来进行打印
System.out.printf("[%s,%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//对客户端发来的请求进行响应处理
// 这里的针对 TCP socket 的读写就和文件读写是一致的
try(InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream=clientSocket.getOutputStream()) {
// 循环的处理每个请求, 分别返回响应
Scanner input = new Scanner(inputStream);
while(true){
//读取请求
//已经读取完,不会再输入新的内容
while(!input.hasNext()){
//返回客户端的ip地址以及端口号
System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
//对请求进行处理
String request=input.next();
// 2. 根据请求, 计算响应
String response = process(request);
// 3. 把这个响应返回给客户端
// 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
//读取返回响应所输入的内容用printWriter包裹,返回
PrintWriter printWriter = new PrintWriter(outputStream);
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) {
e.printStackTrace();
} finally {
// 要关闭,因为资源时有限的,一个服务器不止为1个客户端进行服务,用了之后要及时关闭
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TCPServer tcpServer=new TCPServer(9090);
tcpServer.start();
}
}
4.3.2用线程池进行优化
对服务器代码进行修改:
package TCP;
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;
import java.util.concurrent.*;
public class TCPServer {
private ServerSocket serverSocket = null;
public TCPServer(int port) throws IOException {
serverSocket=new ServerSocket(port);//给服务器指定端口号
}
public void start() throws IOException {
System.out.println("服务器启动");
ExecutorService pool=Executors.newCachedThreadPool();
while(true){
// 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
// accept 就是在 "接电话", 接电话的前提是, 有人给你打了, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
// accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
// 进一步讲, serverSocket 就干了一件事, 接电话
//进一步来说是建立了连接然后应答上,用clientSocket来进行后续操作
Socket clientSocket = serverSocket.accept();
//对接收的响应进行处理
//通过线程池来实现
pool.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
public void processConnection(Socket clientSocket) throws IOException {
//IP地址和端口来进行打印
System.out.printf("[%s,%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//对客户端发来的请求进行响应处理
// 这里的针对 TCP socket 的读写就和文件读写是一致的
try(InputStream inputStream=clientSocket.getInputStream()){
try(OutputStream outputStream=clientSocket.getOutputStream()) {
// 循环的处理每个请求, 分别返回响应
Scanner input = new Scanner(inputStream);
while(true){
//读取请求
//已经读取完,不会再输入新的内容
while(!input.hasNext()){
//返回客户端的ip地址以及端口号
System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
// 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
//对请求进行处理
String request=input.next();
// 2. 根据请求, 计算响应
String response = process(request);
// 3. 把这个响应返回给客户端
// 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
//读取返回响应所输入的内容用printWriter包裹,返回
PrintWriter printWriter = new PrintWriter(outputStream);
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) {
e.printStackTrace();
} finally {
// 要关闭,因为资源时有限的,一个服务器不止为1个客户端进行服务,用了之后要及时关闭
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TCPServer tcpServer=new TCPServer(9090);
tcpServer.start();
}
}
最后用TCP实现一个翻译功能(服务器作出一定修改,客户端不改)
package TCP;
import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
public class TcpDictSever extends TCPServer{
private HashMap<String,String> map = new HashMap<>();
public TcpDictSever(int port) throws IOException {
super(port);
map.put("cat","猫");
map.put("pig","猪");
map.put("dog","狗");
}
@Override
public String process(String request) {
return map.getOrDefault(request,"当前词组无法找到!");
}
public static void main(String[] args) throws IOException {
TcpDictSever tcpDictSever=new TcpDictSever(9090);
tcpDictSever.start();
}
}
感谢观看~
?
?
|