网络编程02:基于TCP的Socket通信
标签: 网络编程
Socket通信模型
我们已经知道,网络编程实际上就是Socket编程。因此我们的网络通信就是要在客户机和服务器的两端各自建立一个Socket,然后进行通信。Socket通信模型如下图所示:
服务器端
创建服务器端类
创建一个ServerSocket的对象,通过构造器指明自身的端口号
调用accpet()方法,此时线程会阻塞,等待并接收连接请求
接收到请求后,accpet()方法返回一个Socket对象;
调用ServerSocket对象的getInputStream()或getOutputStream()来进行通信
关闭相应的资源
客户机
创建客户机类
创建一个Socket的对象,通过构造器指明服务端的IP地址,以及其接收程序的端口号
调用ocket对象的getOutputStream()方法来发送数据,方法返回OutputStream的对象
具体写入过程
关闭相应资源
Socket类详解
public class Socket extends Object implements Closeable
该类实现客户端套接字(也称为“套接字”)。 套接字是两台机器之间通讯的端点。
构造方法
Socket()
创建一个未连接的套接字,并使用系统默认类型的SocketImpl。Socket(InetAddress address, int port)
创建流套接字并将其连接到指定IP地址的指定端口号。Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
创建套接字并将其连接到指定的远程端口上指定的远程地址。Socket(String host, int port)
创建流套接字并将其连接到指定主机上的指定端口号。Socket(String host, int port, InetAddress localAddr, int localPort)
创建套接字并将其连接到指定远程端口上的指定远程主机。
说明:
我们最常用的就是Socket(String host, int port)
,指定要连接的主机名和端口号。
除了第一个不带参的构造方法,其余的构造方法都试图与服务器创建连接,如果连接成功,就返回Socket对象;如果因为某些原因连接失败,则会抛出异常
除了第一个不带参数的构造方法, 其他构造方法都需要在参数中设定服务器的地址, 包括服务器的IP地址或主机名, 以及端口
获取Socket的信息的方法
在一个Socket 对象中同时包含了远程服务器的IP地址和端口信息, 以及客户本地的IP 地址和端口信息。
此外, 从Socket对象中还可以获得输出流和输入流,分别用于向服务器发送数据, 以及接收从服务器端发来的数据。
以下方法用于获取Socket的有关信息:
InetAddress getInetAddress()
: 获得远程服务器的IP地址.int getPort()
: 获得远程服务器的端口.InetAddress getLocalAddress()
: 获得客户本地的IP地址.int getLocalPort()
: 获得客户本地的端口.InputStream getInputStream()
: 获得输入流. 如果Socket 还没有连接, 或者已经关闭, 或者已经通过shutdownInput()
方法关闭输入流, 那么此方法会抛出IOException.OutputStream getOutputStream()
: 获得输出流, 如果Socket 还没有连接, 或者已经关闭, 或者已经通过shutdownOutput()
方法关闭输出流, 那么此方法会抛出IOException.
对Socket进行操作的方法
void bind(SocketAddress bindpoint)
将套接字绑定到本地地址。void close()
关闭此套接字。void connect(SocketAddress endpoint)
将此套接字连接到服务器。void connect(SocketAddress endpoint, int timeout)
将此套接字连接到具有指定超时值的服务器。void shutdownInput()
将此套接字的输入流放置在“流的末尾”。 发送到套接字的输入流侧的任何数据都被确认,然后静默丢弃。
如果您在套接字上调用此方法后从套接字输入流读取,则流的available方法将返回0,其read方法将返回-1 (流结束)。void shutdownOutput()
禁用此套接字的输出流。 对于TCP套接字,任何先前写入的数据将被发送,随后是TCP的正常连接终止序列。 如果在套接字上调用shutdownOutput()之后写入套接字输出流,则流将抛出IOException。
ServerSocket类
Socket类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候你构造的套接字,现在,假如你想实施一个服务器应用,例如一个HTTP服务器或者FTP服务器,你需要一种不同的做法。这是因为你的服务器必须随时待命,因为它不知道一个客户端应用什么时候会尝试去连接它。为了让你的应用能随时待命,你需要使用java.net.ServerSocket类。这是服务器套接字的实现。
ServerSocket和Socket不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服务器套接字获得一个连接请求,它创建一个Socket实例来与客户端进行通信。
服务器端建立套接字的时候,要建立ServerSocket类的对象,调用其accpet()方法,等待并接收连接请求。此时线程会阻塞,当接收到连接请求的时候,accept()方法会返回一个Socket对象。
public class ServerSocket extends Object implements Closeable
这个类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。
构造方法
ServerSocket()
创建未绑定的服务器套接字。ServerSocket(int port)
创建绑定到指定端口的服务器套接字。ServerSocket(int port, int backlog)
创建服务器套接字并将其绑定到指定的本地端口号,并指定了积压。ServerSocket(int port, int backlog, InetAddress bindAddr)
创建一个具有指定端口的服务器,侦听backlog和本地IP地址绑定。
主要方法
Socket accept()
侦听要连接到此套接字并接受它。
实际上,accept方法是唯一用到的方法。
简单例题
客户端给服务端发送信息,服务端输出此信息到控制台上。
服务器端:
package charNet;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TestTCPServer {
public static void server() {
ServerSocket serverSocket = null;
Socket s = null;
InputStream is = null;
try {
//1. 创建一个ServerSocket的对象,通过构造器指明自身的端口号
serverSocket = new ServerSocket(9090);
//2.调用其accpet()方法,返回一个Socket对象
s = serverSocket.accept();
//3.调用ServerSocket对象的getInputStream()获取一个从客户端发送过来的输入流
is = s.getInputStream();
//4.对获取的输入流进行的操作
byte[] b = new byte[20];
int len;
while ((len = is.read(b)) != -1) {
String string = new String(b,0,len);
System.out.println(string);
}
System.out.println("收到来自于" + s.getInetAddress().getHostAddress() + "的连接");
} catch (IOException e) {
e.printStackTrace();
} finally {
//5.关闭相应的资源
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
server();
}
}
客户端:
package charNet;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
//客户端给服务端发送信息,服务端输出此信息到控制台上
//网络编程实际上就是Socket编程
public class TestTCPClient {
//客户机
public static void client() {
Socket socket = null;
OutputStream os = null;
try {
//1. 创建一个Socket的对象,通过构造器指明服务端的IP地址,以及其接收程序的端口号
socket = new Socket(InetAddress.getByName("127.0.0.1"), 9090);
//2.getOutputStream():发送数据,方法返回OutputStream的对象
os = socket.getOutputStream();
//具体的写入过程
os.write("我是客户端".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭相应资源
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
client();
}
}
我们要注意一点,我们在进行运行时,我们要先运行服务器端,在运行客户机端。
运行结果:
//服务器端的控制台输出:
我是客户端
收到来自于127.0.0.1的连接
进阶例题
客户端给服务端发送信息,服务端收到信息后给客户端一个响应,服务器端和客户端都输出信息到控制台上。
服务器端;
package charNet;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class LoginServer {
public static void main(String[] args) {
try {
//1. 创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket = new ServerSocket(8889);
//2. 调用ServerSocket的accept()方法开始监听,等待客户端的连接
System.out.println("***服务器即将启动,等待客户端的连接***");
Socket socket = serverSocket.accept();
//3. 获取输入流,用来读取客户端发送的信息
InputStream is = socket.getInputStream(); //字节输入流
InputStreamReader isr = new InputStreamReader(is); //将字节流转换为字符流
BufferedReader br = new BufferedReader(isr); //为输入流添加缓冲
String info = null;
while ((info = br.readLine()) != null) { //循环读取客户端的信息
System.out.println("我是服务器,客户端说:" + info);
}
socket.shutdownInput(); //关闭输入流
//4. 获取输出流,相应客户端的请求
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os); //包装为打印流
pw.write("服务器表示欢迎!");
pw.flush();
//5. 关闭资源
pw.close();
br.close();
isr.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("服务器结束");
}
}
}
客户机端:
package charNet;
import java.io.*;
import java.net.Socket;
public class LoginClient {
public static void main(String[] args) {
try {
//1. 客户端创建一个Socket,指定服务器地址和端口
Socket socket = new Socket("localhost",8889);
//2. 获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream(); //字节输出流
PrintWriter pw = new PrintWriter(os); //将输出流包装为打印流
pw.write("用户名:user1;密码:123");
pw.flush();
socket.shutdownOutput(); //关闭输出流
//3. 获取输入流,用来读取服务端的响应信息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while ((info = br.readLine()) != null) { //循环读取客户端的信息
System.out.println("我是客户端,服务端说:" + info);
}
//4. 关闭资源
br.close();
pw.close();
os.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
}
服务器端输出:
***服务器即将启动,等待客户端的连接***
我是服务器,客户端说:用户名:admin;密码:123
服务器结束
客户机端输出:
我是客户端,服务端说:服务器表示欢迎!
综合例题
应用多线程来实现服务器与多客户端之间的通信:两个客户端分别发送数据,服务器为每个客户端单独建立一个线程来通信,接收各客户端发送的数据后,写到一个共享文档中。
多线程服务器的基本步骤:
- 服务器创建ServerSocket,循环调用accept()等待客户端连接
- 客户端创建一个socket并请求和服务器端连接
- 服务器端接受客户端请求,创建socket与该客户建立专线连接
- 建立连接的两个Socket在一个单独的线程上对话
- 服务器端继续等待新的连接
客户机A
package charNet;
/**
* Created by japson on 9/14/2017.
*/
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
/**
* 1. 创建一个Socket对象,用来指定服务器的地址和端口号
* 2. 创建一个输出流,用于发送数据
* 3. 创建一个输入流,用于从文件读取数据到程序,在将数据写到输出流中
* 4. 获取服务器的响应
*/
public class TestFileClientA {
public static void fileClientA() {
Socket socket = null;
BufferedWriter bw = null;
BufferedReader br = null;
InputStream is = null;
try {
socket = new Socket(InetAddress.getByName("localhost"),9999);
OutputStream os = socket.getOutputStream();
bw = new BufferedWriter(new OutputStreamWriter(os));
br = new BufferedReader(new FileReader("ClientA.txt"));
String str = null;
while ((str = br.readLine()) != null) {
bw.write(str);
bw.newLine();
bw.flush();
}
socket.shutdownOutput();
is = socket.getInputStream();
byte[] b = new byte[1024];
int len;
while ((len = is.read(b)) != -1) {
String str1 = new String(b,0,len);
System.out.println(str1);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
fileClientA();
}
}
客户机B
package charNet;
/**
* Created by japson on 9/14/2017.
*/
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
/**
* 1. 创建一个Socket对象,用来指定服务器的地址和端口号
* 2. 创建一个输出流,用于发送数据
* 3. 创建一个输入流,用于从文件读取数据到程序,在将数据写到输出流中
* 4. 获取服务器的响应
*/
public class TestFileClientB {
public static void fileClientB() {
Socket socket = null;
BufferedWriter bw = null;
BufferedReader br = null;
InputStream is = null;
try {
socket = new Socket(InetAddress.getByName("localhost"),9999);
OutputStream os = socket.getOutputStream();
bw = new BufferedWriter(new OutputStreamWriter(os));
br = new BufferedReader(new FileReader("ClientB.txt"));
String str = null;
while ((str = br.readLine()) != null) {
//newLine()不能放在flush()之后??
bw.write(str);
bw.newLine();
bw.flush();
}
socket.shutdownOutput();
is = socket.getInputStream();
byte[] b = new byte[1024];
int len;
while ((len = is.read(b)) != -1) {
String str1 = new String(b,0,len);
System.out.println(str1);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
fileClientB();
}
}
服务器
package charNet;
/**
* Created by japson on 9/14/2017.
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 要求:创建一个多线程服务器,循环监听客户端的请求,在收到客户端发送的文件后保存到本地,并给出响应
* 对于多线程,什么是共享数据?socket?
* 步骤:
* 1. 创建一个ServerSocket对象,用来确定端口号
* 2. 循环调用其accept方法监听,并返回监听到的Socket对象
* 3. 启动子线程类
* 线程类,采用runnable接口:
* 1. 在类中定义一个Socket,但是不初始化,需要用构造函数来给它初始化
* 2. 在run()方法中创建一个输入流,将接收到的信息输入到程序中
* 3. 在创建一个输出流,将程序中的数据写到本地
* 4. 返回给服务器一条信息(通过输出流)
*/
class ServerThread implements Runnable {
Socket socket;
FileWriter fileWriter;
public ServerThread (Socket socket,FileWriter fw) {
this.socket = socket;
this.fileWriter = fw;
}
public void run() {
BufferedReader br = null;
BufferedWriter bw = null;
OutputStream os = null;
try {
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
br = new BufferedReader(isr);
bw = new BufferedWriter(fileWriter);
String str = null;
while ((str = br.readLine()) != null) {
bw.write(str);
bw.flush();
bw.newLine();
}
os = socket.getOutputStream();
os.write("服务器已经成功接受你发送的文件!".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
// try {
// os.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// try {
// bw.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
// try {
// socket.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
}
}
}
public class TestFileServer {
public static void fileServer() {
try {
ServerSocket serverSocket = new ServerSocket(9999);
Socket socket = null;
int count = 0;
FileWriter fw = new FileWriter("共享文档.txt",true);
System.out.println("---服务器启动,等待客户端的连接---");
while (true) {
count++;
socket = serverSocket.accept();
ServerThread serverThread = new ServerThread(socket,fw);
Thread thread = new Thread(serverThread);
thread.setName("线程"+ count);
thread.start();
System.out.println("客户端的数量:" + count);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
fileServer();
}
}
问题
客户端,将本地的内容写入到输出流时,使用缓冲流。若
newLine()
放在flush()
之后,则异常;放在flush()
方法之前,则可以。问题是:在其他代码中,newLine()
可以放在flush()
之后,为什么这里不可以?服务器端的SeverThread类的run()方法,如果在该方法中关闭IO流和socket,那么第一个客户端启动后,第二个客户端启动时,服务器报错。如果不关闭资源,则可以。
问题是:如何关闭资源
更新 问题2:
问题2的提出有误。
首先,对于每一个线程,关闭相关资源是可行的也是必要的,每个线程中的IO流都是独立的,相互不影响的。
其次,根据对不同代码的注释,最后将问题代码定位在所在:
bw.close();
os.close();
因为我们是不同的线程中的数据,对同一共享文档进行操作。因此要想关闭bw对象bw = new BufferedWriter(fileWriter);
,就同时关闭了fileWriter对象。但是fileWriter对象是在主线程中定义的,每一个线程传递的数据都要写入文档,因此不能在线程内进行关闭。
最后,注释掉bw.close();
后,运行成功