计网实验二
实验目的
学习采用Socket套接字设计简单的网络数据收发程序,理解应用数据包是如何通过传输层进行传送的
实验原理
套接字
-
应用程序通过Socket套接字来发送和接收数据
-
Socket允许应用程序添加到网络中,可以和处于同一个网络中的其他应用程序进行通信
-
TCP/IP协议族中的主要socket类型为流套接字(sockets sockets)和数据报套接字(datagram sockets)
流套接字:将TCP作为其端对端协议(底层使用IP协议),提供了一个可信赖的字节流服务。一个TCP/IP流套接字代表了TCP连接的一端
**数据报套接字:**用UDP协议(底层同样使用IP协议),提供了一个"尽力而为"(best-effort)的数据报服务,应用程序可以通过它发送最长65500字节的个人信息。
-
一个TCP/IP套接字由IP地址、端对端协议(TCP、UDP协议)和一个端口号来唯一确定
多线程\线程池对比
当一个客户端向一个已经被其他客户端占用的服务器发送连接请求时,虽然其在连接建立后即可向服务器端发送数据,服务器端在处理完已有客户端的请求前,却不会对新的客户端作出响应。
多线程
-
创建多个线程会占用CPU资源
-
创建多个线程会消耗系统内存
-
一旦发生阻塞,会频繁的进行上下文切换,导致CPU使用更少的时间来处理连接服务
-
额外的线程会增加客户端总服务的时间
线程池
服务器启动时会创建一个固定数量的线程组成的线程池
客户端发送一个请求给服务端,会交给线程池中的一个线程进行处理,当线程处理完毕后,又返回到线程池中,为下一次请求做好准备
如果线程池中所有的线程都被占用,那么这次请求会在队列中等待,直到有空闲的线程可以用
实验步骤
1:首先实现的是一对一本机客户端和服务端之间的通信
服务器SocketServe
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServe {
public static void main(String[] args)throws Exception
{
ServerSocket serve=new ServerSocket(5209);
System.out.println("服务器启动成功");
Socket socket=serve.accept();//阻塞函数:服务器启动后阻塞在此,等待客户端响应
BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));//获取客户端数据
BufferedReader out=new BufferedReader(new InputStreamReader(System.in));//获取服务端输入的数据
PrintWriter pw=new PrintWriter(socket.getOutputStream());
while(true)
{
System.out.println("Client:>"+in.readLine());//阻塞函数:等待客户数据
String str= out.readLine();
pw.println(str);
pw.flush();
}
}
}
客户端SocketClient
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class SocketClient {
public static void main(String[] args)throws IOException
{
Socket socket = new Socket("10.72.45.249", 5209);//获取服务端IP地址和端口
System.out.println("客户端启动成功");
BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));//获取服务端数据
BufferedReader out=new BufferedReader(new InputStreamReader(System.in));//获取客户端输入的数据
PrintWriter pw=new PrintWriter(socket.getOutputStream());//从socket中获取待发送的数据
while(true)
{
String str= out.readLine();
pw.println(str);
pw.flush();
System.out.println("Serve:>"+in.readLine());//阻塞函数,等待服务端数据
}
}
}
分别启动服务器和客户端
互发消息如下
客户端发送消息:“你好,服务端!”,服务器接收到:”你好,服务端!“
服务器发送消息:”你好,客户端!“,客户端收到:”你好,客户端!“
- 这种方法只能客户端或者服务器发送一条并且是回合制的对话消息
客户端可以连发两条消息,但是服务器只能接收到客户端发送的最后一条消息;同理,服务器也如此
- 只能是客户端先发送消息,解除在服务器中的阻塞函数后,才能够正常进行通话
+++
2、实现一对一的局域网下的通信
分为服务器(SocketServe)和客户端(SocketClient),服务器和客户端都包含两个线程(多线程)
SocketServe:S_ReceiveThread线程(数据接收监听线程)、S_SendThread线程(数据发送监听线程)
SocketClient:C_ReceiveThread线程(数据接收监听线程)、C_SendThread线程(数据发送监听线程)
SocketServe:
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServe {
public static void main(String[] args)throws Exception
{
ServerSocket serve=new ServerSocket(5209);
System.out.println("服务器启动成功");
while(true)
{
Socket socket=serve.accept();//阻塞函数
new Thread(new S_ReceiveThread(socket)).start();//开启ServeThread线程
new Thread(new S_SendThread(socket)).start();//设置System.in监听
}
}
}
SocketClient:
import java.io.IOException;
import java.net.Socket;
public class SocketClient {
public static void main(String[] args)throws IOException
{
Socket socket=new Socket("10.72.45.249",5209);
System.out.println("连接服务器成功!");
new Thread(new C_SendThread(socket)).start();//监听控制台输入
new Thread(new C_ReceiveThread(socket)).start();//监听数据接收
}
}
S_ReceiveThread:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class S_ReceiveThread implements Runnable{
public Socket socket;
public S_ReceiveThread(Socket socket) {this.socket=socket;}
@Override
public void run() {
try {
BufferedReader receive=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取数据
while(true)
{
String str=receive.readLine();//阻塞函数,等待输入
System.out.println("Client:"+socket.getPort()+">"+str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
S_SendThread:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class S_SendThread implements Runnable{
public Socket socket;
public S_SendThread(Socket socket) {this.socket=socket;}
@Override
public void run() {
try {
BufferedReader send=new BufferedReader(new InputStreamReader(System.in));//控制台输入
while(true)
{
PrintWriter pw=new PrintWriter(socket.getOutputStream());
String str=send.readLine();//等待控制台输入
pw.println(str);
pw.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
C_ReceiveThread:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class C_ReceiveThread implements Runnable{
public Socket socket;
public C_ReceiveThread(Socket socket)
{
this.socket=socket;
}
@Override
public void run()
{
try{
BufferedReader receive=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取数据
while(true)
{
String str=receive.readLine();//阻塞函数,等待数据输入
System.out.println("Serve:>"+str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
C_SendThread:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class C_SendThread implements Runnable{
public Socket socket;
public C_SendThread(Socket socket)
{
this.socket=socket;
}
@Override
public void run()
{
try{
BufferedReader send=new BufferedReader(new InputStreamReader(System.in));//控制台输入
while(true)
{
PrintWriter pw=new PrintWriter(socket.getOutputStream());
String str=send.readLine();//等待控制台输入
pw.println(str);
pw.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
可以实现多天消息发送,不是回合制的发送信息,并且可以先有Serve来发送,不必先由Client发送
可以实现在同一个WLAN下的通信
+++
3、实现一对一的通信,使用线程池来管理线程
修改的文件为SocketServe、SocketClient
SocketServe:
public class SocketServe {
public static void main(String[] args)throws Exception
{
ExecutorService pool= Executors.newFixedThreadPool(2);//创建线程池
ServerSocket serve=new ServerSocket(5209);
System.out.println("服务器启动成功");
while(true)
{
Socket socket=serve.accept();//阻塞函数
System.out.println(socket.getPort()+"已上线!");
pool.submit(new S_ReceiveThread(socket));
pool.submit(new S_SendThread(socket));
pool.shutdown();
}
}
}
SocketClient:
public class SocketClient {
public static void main(String[] args)throws IOException
{
ExecutorService pool= Executors.newFixedThreadPool(2);
Socket socket=new Socket("10.72.45.249",5209);
System.out.println("连接服务器成功!");
pool.submit(new C_ReceiveThread(socket));//使用线程池调用线程
pool.submit(new C_SendThread(socket));//使用线程池调用线程
pool.shutdown();
}
}
+++
4、增加文件互传功能功能
服务器发送文件
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class S_FileThread{
public static void main(String[] args) throws IOException {
FileInputStream fis = null;
DataOutputStream dos =null;
ServerSocket serve=new ServerSocket(5209);
Socket socket=serve.accept();//阻塞函数,监听函数
System.out.println("建立连接成功!");
System.out.println("请输入要传输文件的相对路径:如D:\\1.txt");
BufferedReader send=new BufferedReader(new InputStreamReader(System.in));//控制台输入
String str=send.readLine();//等待控制台输入
File file=new File(str);//输入要传输文件的相对路径
try{
if(file.exists())
{
fis=new FileInputStream(file);
dos=new DataOutputStream(socket.getOutputStream());
dos.writeUTF(file.getName());//文件名传输
dos.flush();
dos.writeLong(file.length());//文件长度
dos.flush();
System.out.println("文件开始传送");
int length;
byte[] bytes=new byte[1024];
while((length=fis.read(bytes,0,bytes.length))!=-1)
{
dos.write(bytes,0,length);
dos.flush();
}
System.out.println();
System.out.println("文件传输成功!");
}
} catch (IOException e) {
e.printStackTrace();
}
try
{
fis.close();
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端接收文件
import java.io.*;
import java.net.Socket;
public class C_FileThread {
public static void main(String[] args) throws IOException {
DataInputStream dis=null;
FileOutputStream fos = null;
Socket socket= new Socket("10.72.45.249",5209);
System.out.println("连接服务器成功!");
try{
System.out.println("请输入要保存的位置,如:C:\\1");
BufferedReader send=new BufferedReader(new InputStreamReader(System.in));//控制台输入
String str=send.readLine();//等待控制台输入
File directory = new File(str);//获取要保存的文件目录下
dis = new DataInputStream(socket.getInputStream());
String filename = dis.readUTF();//获取发送来的文件名
System.out.println("服务器发送的文件名:"+filename);
long fileLength = dis.readLong();//获取发送来的字节长度
System.out.println("文件长度:"+fileLength+"字节");
if (!directory.exists()) {
directory.mkdir();
}
File file = new File(directory.getAbsoluteFile() + File.separator + filename);
fos = new FileOutputStream(file);
System.out.println("文件正在传送");
int length;
byte[] bytes = new byte[1024];
while ((length = dis.read(bytes, 0, 1024))!=-1)
{
fos.write(bytes, 0, length);
fos.flush();
}
fos.close();
System.out.println();
System.out.println("文件接收成功[File Name" + filename + "],Size" + fileLength+"字节");
} catch (IOException e) {
e.printStackTrace();
}
try{
fos.close();
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
+++
这次实验中,涉及到的知识点有
-
socket嵌套字
-
服务端设置端口、监听函数
ServerSocket serve=new ServerSocket(5209);//设置端口 Socket socket=serve.accept();//阻塞函数
-
客户端获取服务器IP和端口号
Socket socket= new Socket("10.72.45.249",5209);
-
-
相关输入流、输出流
-
获取控制台输入
BufferedReader send=new BufferedReader(new InputStreamReader(System.in));//控制台输入 String str=send.readLine();//等待控制台输入
-
获取嵌套字数据
BufferedReader receive=new BufferedReader(new InputStreamReader(socket.getInputStream())); String str=send.readLine();//等待输入流
-
-
文件内容读取
-
定义文件流和数据流
fis=new FileInputStream(file); dos=new DataOutputStream(socket.getOutputStream());
-
发送和获取文件名、文件内容长度
dos.writeUTF(file.getName());//文件名传输 dos.flush(); dos.writeLong(file.length());//文件长度 dos.flush(); String filename = dis.readUTF();//获取发送来的文件名 System.out.println("服务器发送的文件名:"+filename); long fileLength = dis.readLong();//获取发送来的字节长度 System.out.println("文件长度:"+fileLength+"字节");
-
对数据流的读写
int length; byte[] bytes=new byte[1024]; while((length=fis.read(bytes,0,bytes.length))!=-1) { dos.write(bytes,0,length); dos.flush(); }
-
-
线程管理和线程池的使用
ExecutorService pool= Executors.newFixedThreadPool(2);//创建线程池 while(true) { Socket socket=serve.accept();//阻塞函数 pool.submit(new S_ReceiveThread(socket));//接收信息线程 pool.submit(new S_SendThread(socket));//发送信息线程 pool.shutdown(); }
-
同步异步问题
涉及到阻塞函数,双方等待对方控制台输入和socket流
在第4步中,本来打算是将文件传输放入到线程中执行,但是实践中发现服务器发送完文件后,客户端无法知晓是否已经文件传输完毕,导致文件传送完毕后,无法再继续进行通信,出现阻塞问题,以下为参考链接