目录
ServerSocket与Socket的介绍
TCP的socket api中涉及到两个重要的类:ServerSocket & Socket;
ServerSocket是专门供给服务器使用

ServerSocket的构造方法中自带端口号,以及ServerSocket的对象包含一个accept方法,是一个类似于接通功能的方法
Socket是给客户端和服务器都提供服务,Socket的构造方法中可以指定要连接的服务器建立连接(类似拨号的过程)

++TCP的socket中是通过Inputstream和Outputstream来进行读写操作的++,通过这两个方法来获取到socket内部的流对象。
TCP是字节流,也就是传输的基本单位就是字节
ServerSocket与Socket的功能是不同的,对于服务器来说需要上来先与客户端建立连接,建立连接要使用ServerSocket的对象的accept方法,方法的返回值为socket类型;服务器一启动就会执行到建立连接的位置,如果此时没有客户端连接那么accept就会进入阻塞状态,直到有客户端连接。
java
private ServerSocket serverSocket = null;
public MyTcpSocket(int port) throws IOException {
Socket socket = serverSocket.accept();
}
简单可以理解为:ServerSocket只是用来与客户端建立连接,连接后将socket对象交给socket来进行后续操作。
服务器的实现
每创建一个服务器对象都要创建一个ServerSocket来连接客户端,创建服务器时要包含端口号,不然客户端要连接的话没有端口号就无法连接。
java
class MyTcpSocket{
private ServerSocket serverSocket = null;
public MyTcpSocket(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
}
然后需要在服务器中创建start方法用来在服务器执行客户端的请求,而客户端不会只发来一条请求好比打电话时不能打一次电话只说一句话,所以在++start方法中要不停的处理请求++;一台服务器启动后要不停的处理客户端的请求就要不停的与客户端建立连接;此时代码可以写为:
java
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
Socket socket = serverSocket.accept();
possess(socket);
}
}
处理请求的部分放在possess方法中,将与客户端建立连接的socket传入方法中。
在Tcp字节流模式下的读写操作要使用inputStream和OutputStream,而这两个类使用后要进行回收(防止资源泄露,避免数据丢失),我们使用try-catch方法的try with source用法,在程序结束后会自动回收资源;
在try的内部首先读取inputStream读到的请求,直接read读到的请求是byte【】需要转换成String,而这里**++直接使用Scanner读取的话会直接转换成String类型++**;
执行请求时也要不停的处理请求,也就是while(true)的形式,代码可以写为:
java
private void possess(Socket clientSocket) {
System.out.println("客户端上线");
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
while(true){
String request = scanner.next();
}
} catch (IOException e) {
e.printStackTrace();
}
}
scanner.next的读取方式是读取到空白符会停下来,所以要约定客户端在输入请求时要在每个请求的末尾加上空白符。
TCP是按照字节来传输,程序员希望若干个字节可以构成一个应用层数据包,那么该如何区分哪里到哪里是一个应用层数据包呢?可以通过分割符的方式来区分,上述的空白符就作为这里的分隔符。
将读取到的请求放到possessFun中来执行请求,执行的结果返回到possess后将结果发送给客户端
java
String response = possessFun(request);
outputStream.write(response.getBytes());
因为是回响服务器,所以在possessFun中将请求直接返回即可。
这里需要注意,在读取请求前要先判断是否有请求输入,next是带有阻塞功能的所以可以使用scanner的next来判断请求的到达,如果请求到了并且带有明确的分隔符就会返回true,如果tcp断开连接就会返回false,此时可以提示客户端断开连接。
使用scanner读文件读到文件末尾或者TCP断开连接就会返回false,否则就会阻塞等待客户端继续发送请求,这个过程是Tcp连接断开->阻塞解除返回false;Tcp没断开连接->对方没有发数据过来,scanner阻塞->客户端发请求过来->解除阻塞并返回true;
服务器的完整代码:
java
class MyTcpSocket{
private ServerSocket serverSocket = null;
public MyTcpSocket(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
Socket socket = serverSocket.accept();
possess(socket);
}
}
private void possess(Socket clientSocket) {
System.out.println("客户端上线");
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);
while(true){
if(! scanner.hasNext()){
System.out.println("客户端下线");
break;
}
String request = scanner.next();
String response = possessFun(request);
outputStream.write(response.getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
}
private String possessFun(String request) {
return request+" \n";
}
}
客户端的实现
首先构造方法中有服务器的ip地址和端口号,这样构造的过程中就会与服务器建立连接。
java
private Socket socket = null;
public MyclientSocket(String address,int port) throws IOException {
socket = new Socket(address,port);
}
在客户端里同样建立一个start方法,方法中也使用try with sources的方法实例化字节流对象;
在控制台输入请求,所以这里使用Scanner,输入的语句使用String来接收。
将接收的语句发给服务器前记得在末尾加上\n作为分隔符,发送给服务器使用outputStream,传输的单位是字节所以将请求转化为byte发送。
发送了请求后等待服务器返回响应,如同服务器一样++直接使用scanner来接收字节流响应可以直接转换为字符串++,将字符串响应输出到控制台即可完成这一套客户端搭配服务器完成的任务。
java
class MyclientSocket{
private Socket socket = null;
public MyclientSocket(String address,int port) throws IOException {
socket = new Socket(address,port);
}
public void start(){
System.out.println("客户端启动");
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();){
Scanner scanner = new Scanner(System.in);
Scanner Resscanner = new Scanner(inputStream);
while(true){
String request = scanner.next();
request += "\n";
outputStream.write(request.getBytes());
if(!Resscanner.hasNext()){
break;
}
String reponse = Resscanner.next();
System.out.println(reponse);
}
}catch(IOException e){
e.printStackTrace();
}
}
}
执行完这些的步骤是:(1)客户端从控制台得到请求 (2)将请求发送到服务器 (3)服务器接收请求(4)服务器处理请求得到响应 (5)将响应发送到客户端 (5)客户端接收响应 (7)将响应输出在控制台。
代码优化
1、对于服务器代码那边创建的socket对象没有关闭操作
服务器的ServerSocket的对象不需要特别关闭因为生命周期是跟随服务器进程的,客户端的Socket的对象也不需要特别关闭,同理也是如此,当客户端生命周期结束后socket也会结束,而服务器需要对应多个客户端,每个客户端都对应一个socket,如果用完了不关闭就会使文件描述符得不到释放而导致文件泄露。
为了保证能在程序结束再结束socket所以使用finally来close

二、当前代码无法为多个客户端提供服务
当一个客户端接通accept连接到服务器后就会进入循环,此时另一个客户端想要连接服务器就会因为此时服务器正在循环中无法accept,解决这个问题就要引入多线程/线程池。
多线程:
主线程用来处理accept,每接收一个accept就创建一个线程来提供服务
线程池:
这里使用可以自动扩容的线程池

TCP与UDP协议的服务器自己实现都已经完成
感谢观看
道阻且长,行则将至