目录
TCP流套接字编程
TCP用的协议比UDP更多,可靠性
提供的api主要有两个类ServerSocket(给服务器使用的socket),Socket(既会给服务器使用也会给客户端使用)
ServerSocket API
ServerSocket
是创建TCP服务端Socket的API。
ServerSocket
构造方法:
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket
方法:
方法签名 | 方法说明 |
---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
Socket API
Socket
是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket
构造方法:
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket
方法:
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
TCP回显客户端服务器
TCP版本的回显服务器,进入循环之后要做的事情不是读取客户端的请求,而是先处理客户端的“连接”
服务器代码流程:
- 读取请求并分析
- 根据请求计算响应
- 把响应写回客户端
//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了
Socket clientSocket = serverSocket.accept();
//把内核中的连接获取到应用程序中了=>这个过程类似于“生产者消费者模型”
一次0,主要是经历两个部分
- 等(阻塞)
- 拷贝数据
TCP中涉及到两种socket
- serverSocket
- clientSocket
Socket clientSocket = serverSocket.accept();
//TCP通信能实现两台计算机之间的数据交互,通信的两端要严格区分为客户端(Client)与服务端(Server)
通过processConnection
这个方法(自己实现)来处理一个连接的逻辑
try (InputStream inputStream = clientSocket.getInputStream();//相当于耳麦(clientSocket)的耳机(inputStream)
OutputStream outputStream = clientSocket.getOutputStream()) {//相当于耳麦(clientSocket)的麦克风(outputStream)
public class TcpEchoServer {
private ServerSocket serverSocket = null;
//此处不应该创建一个固定线程数目的线程池,不然就有上限了
private ExecutorService service= Executors.newCachedThreadPool();
//这个操作就会绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了
Socket clientSocket = serverSocket.accept();
//把内核中的连接获取到应用程序中了
processConnection(clientSocket);
}
}
//通过这个方法来处理一个连接的逻辑
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
//接下来就可以 读取请求,根据请求计算响应,返回响应 三步走了
// Socket 对象内部包含了两个字节流对象,可以把这两字节流对象,完成后续的读写工作
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//一次连接中,可能会涉及到多次请求/响应
while (true) {
//1、读取请求并解析,为了读取方便,直接使用Scanner
Scanner sc = new Scanner(inputStream);
if (!sc.hasNext()) {
//读取完毕,客户端下线了
System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//这个代码暗含一个约定,客户端发过来的请求得是文本数据,同时还得带有空白符作为分割,(比如换行这种)
String request = sc.next();
//2、根据请求计算响应
String response = process(request);
//3、把响应写回客户端,把OutputStream使用PrinterWriter包裹一下,方便进行发数据
PrintWriter writer = new PrintWriter(outputStream);
// 使用PrintWriter的println方法把响应返回给客户端
// 此处用println而不是print就是为了在结尾加上\n,方便客户端读取响应,使用Scanner.next读取
writer.println(response);
// 还需要加入一个"刷新缓冲区"操作
//网络程序讲究的就是客户端和服务器能“配合”
writer.flush();//“上完厕所冲一下”。
//这里加上 flush 更稳妥,不加也不一定就出错!!缓冲区内置了一定的刷新策略
//比如缓冲区满了,就会触发刷新; 再比如,程序退出,也会触发刷新......推荐大家把 flush 刷新给加上
/*
IO操作是比较有开销的,相比于访问内存。进入IO操作次数越多,程序的速度越慢。
方法:使用一块内存作为缓冲区,写数据的时候,先写到缓冲区,攒一波数据,统一进入IO。
PrintWriter内置了缓冲区,手动刷新,确保这里的数据是真的通过网卡发出去了,而不是残留在内存缓冲区中
*/
//日志,打印当前的请求详情
System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端代码流程:
- 从控制台读取用户的输入
- 把输入的内容构造请求并发送给服务器
- 从服务器读取响应
- 把响应显示到控制台上
public class TcpEchoClient {
private Socket socket=null;
//要和服务器通信,就需要先知道,服务器所在的位置
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//完成这个new操作就完成了tcp连接的建议
socket=new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动");
Scanner scConsole=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
while(true){
//1、从控制台输入一个字符串
System.out.print("-> ");
String request = scConsole.next();
//2、把请求发送给服务器
PrintWriter printWriter=new PrintWriter(outputStream);
//使用println带上换行,后续服务器读取请求,就可以使用Scanner.next来读取了
printWriter.println(request);
//别忘记flush,确保数据真的发出去了
printWriter.flush();
//3、从服务器读取响应
Scanner scNetwork=new Scanner(inputStream);
String response = scNetwork.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",9090);
client.start();
}
}
//以目前以上的代码执行结果
//服务器
服务器启动
[/127.0.0.1:63945] 客户端上线
[/127.0.0.1:63945] req: 你好, resp: 你好
[/127.0.0.1:63945] req: hello, resp: hello
[/127.0.0.1:63945] 客户端下线
//客户端
客户端启动
-> 你好
你好
-> hello
hello
-> //这里客户端退出了
以下是改进方式:
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了
Socket clientSocket = serverSocket.accept();
//把内核中的连接获取到应用程序中了
//单个线程不太方便同时完成多个任务,因此要多线程,主线程主要负责寻找客户端,每有一个客户端就创建一个新的线程
/*
Thread t=new Thread(()->{
processConnection(clientSocket);
});
t.start();
*/
//使用线程池
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
但注意:我们上述的不能写成这种方式
try(Socket clientSocket = serverSocket.accept()){
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
};
/*
processConnection 和主线程就是不同线程了
执行 processConnection 过程中,主线程 try 就执行完毕了。
这就会导致 clientSocket 还没用完呢,就关闭了
因此,还是要把clientSocket交给processConnection里来关闭
*/
完整代码
服务器
public class TcpEchoServer {
private ServerSocket serverSocket = null;
//此处不应该创建一个固定线程数目的线程池,不然就有上限了
private ExecutorService service= Executors.newCachedThreadPool();
//这个操作就会绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while (true) {
//处理连接的过程=>此时可能客户端还没来,accept就阻塞等待了
Socket clientSocket = serverSocket.accept();
//把内核中的连接获取到应用程序中了
//单个线程不太方便同时完成多个任务,因此要多线程,主线程主要负责寻找客户端,每有一个客户端就创建一个新的线程
/*
Thread t=new Thread(()->{
processConnection(clientSocket);
});
t.start();
*/
//使用线程池
service.submit(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
//通过这个方法来处理一个连接的逻辑
//服务器一启动,就会执行accept,并阻塞等待,当客户端连接上之后,就会立即执行这个方法
private void processConnection(Socket clientSocket) {
System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
//接下来就可以 读取请求,根据请求计算响应,返回响应 三步走了
// Socket 对象内部包含了两个字节流对象,可以把这两字节流对象,完成后续的读写工作
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//一次连接中,可能会涉及到多次请求/响应
while (true) {
//1、读取请求并解析,为了读取方便,直接使用Scanner
Scanner sc = new Scanner(inputStream);
//hasNext这里在客户端没有发请求的时候也会阻塞等待,等到客户端真正发数据或者客户端退出,hasNext就返回了
if (!sc.hasNext()) {
//读取完毕,客户端下线了
System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
break;
}
//这个代码暗含一个约定,客户端发过来的请求得是文本数据,同时还得带有空白符作为分割,(比如换行这种)
String request = sc.next();
//2、根据请求计算响应
String response = process(request);
//3、把响应写回客户端,把OutputStream使用PrinterWriter包裹一下,方便进行发数据
PrintWriter writer = new PrintWriter(outputStream);
// 使用PrintWriter的println方法把响应返回给客户端
// 此处用println而不是print就是为了在结尾加上\n,方便客户端读取响应,使用Scanner.next读取
writer.println(response);
// 还需要加入一个"刷新缓冲区"操作
//网络程序讲究的就是客户端和服务器能“配合”
writer.flush();//“上完厕所冲一下”。
//这里加上 flush 更稳妥,不加也不一定就出错!!缓冲区内置了一定的刷新策略
//比如缓冲区满了,就会触发刷新; 再比如,程序退出,也会触发刷新......推荐大家把 flush 刷新给加上
/*
IO操作是比较有开销的,相比于访问内存。进入IO操作次数越多,程序的速度越慢。
方法:使用一块内存作为缓冲区,写数据的时候,先写到缓冲区,攒一波数据,统一进入IO。
PrintWriter内置了缓冲区,手动刷新,确保这里的数据是真的通过网卡发出去了,而不是残留在内存缓冲区中
*/
//日志,打印当前的请求详情
System.out.printf("[%s:%d] req: %s, req: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//在finally中加入close,确保当前socket被及时关闭
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端
public class TcpEchoClient {
private Socket socket=null;
//要和服务器通信,就需要先知道,服务器所在的位置
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
//完成这个new操作就完成了tcp连接的建议
socket=new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动");
Scanner scConsole=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()){
while(true){
//1、从控制台输入一个字符串
System.out.print("-> ");
String request = scConsole.next();
//2、把请求发送给服务器
PrintWriter printWriter=new PrintWriter(outputStream);
//使用println带上换行,后续服务器读取请求,就可以使用Scanner.next来读取了
printWriter.println(request);
//别忘记flush,确保数据真的发出去了
printWriter.flush();
//3、从服务器读取响应
Scanner scNetwork=new Scanner(inputStream);
String response = scNetwork.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",9090);
client.start();
}
}