网络编程套接字
文章目录
前言
网络编程,写代码实现跨主机之间的通信
套接字(socket,是网络编程的函数统称
应用层把数据传输到传输层(操作系统,需要调用传输层的函数,传输层给应用层提供的编程接口,这一组统成为socket,也就是网络编程套接字
一、协议
传输层涉及到两组最主要的协议,TCP协议,UDP协议,特性差别行很大,代码编写差异大,提供了两组socket函数
TCP协议 有连接,可靠传输,面向字节流,全双工
UDP协议 无连接,不可靠传输,面向数据报,全双工
1.连接
连接,网络连接,抽象的概念(不是物理上的,通信双方的两个主机互相保存了对方的核心信息(IP/端口
断开连接就是把对方的关键信息删掉了
TCP在正式通信之前,就需要让通信双方保存对方的关键信息,在通信完毕之后,在删除对方的关键信息 像打电话
UDP来说不需要,直接上来就发送 像发短信
2.可靠传输
可靠传输/不可靠传输
网络通信过程中,可能会出现丢包的客观现象,就像是车流量太大堵车,数据不堵,直接丢包
3.可靠传输
可靠传输就是在对抗丢包,尽可能的把数据报传输给对方 ,就算不能百分百保证送达,也能知道对方是否接收到了数据
UDP不考虑丢不丢包,可靠传输固然有好处,但也会付出代价,(传输效率
4.全双工
全双工
一个通信,可以进行双向传输
半双工
一个通信只能单项传输
二、UDP数据报套接字编程
socket有多重含义
网络编程套接字socket,网络编程中用到的传输层API的统称
UDP提供的API主要有两个函数
1.DatagramSocket
DatagramSocket
这是一个socket对象,相当于对网卡硬件设备的抽象
后续操作这个socket对象,就能够操作网卡
很多方法
⽅法签名 ⽅法说明
DatagramSocket()
创建⼀个UDP数据报套接字的Socket,绑定到本机任
意⼀个随机端⼝(⼀般⽤于客⼾端)
DatagramSocket(int port) 创建⼀个UDP数据报套接字的Socket,绑定到本机指
定的端⼝(⼀般⽤于服务端), port就代表端口号
DatagramSocket ⽅法:
⽅法签名 ⽅法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该
⽅法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发
送)
void close() 关闭此数据报套接字
2.DatagramPacket
DatagramPacket
DatagramPacket 构造⽅法:
⽅法签名 ⽅法说明
DatagramPacket(byte\[\] buf, int length) 构造⼀个DatagramPacket以⽤来接收数据报,接收的
数据保存在字节数组(第⼀个参数buf)中,接收指定
⻓度(第⼆个参数length)
DatagramPacket(byte\[\] buf, int offset, int length,
SocketAddress address)
构造⼀个DatagramPacket以⽤来发送数据报,发送的
数据为字节数组(第⼀个参数buf)中,从0到指定⻓
度(第⼆个参数length)。address指定⽬的主机的IP
和端⼝号
DatagramPacket (一次传输的基本单位)⽅法:
⽅法签名 ⽅法说明
InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发
送的数据报中,获取接收端主机IP地址
int getPort() 从接收的数据报中,获取发送端主机的端⼝号;或从
发送的数据报中,获取接收端主机端⼝号
byte\[\] getData() 获取数据报中的数据
构造UDP发送的数据报时,需要传⼊ SocketAddress ,该对象可以使⽤ InetSocketAddress
来创建。
InetSocketAddress
InetSocketAddress ( SocketAddress 的⼦类 )构造⽅法:
⽅法签名 ⽅法说明
InetSocketAddress(InetAddress addr, int port) 创建⼀个Socket地址,包含IP地址和端⼝号
就是UDP数据传输中的基本单位
接下来使用它,编写UDP服务器,UDP客户端,也就是回显示服务器,相到于网络编程中的hello world
3.代码编写
服务器
Java
public class UdpEchoServer {
//先创建一个socket 对象
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
//port就是端口号 , 此处需要端口号 , 作为构造方法的参数
//端口号区分一个主机上的不同应用程序
//对于服务器来说 , 需要程序员手动设置一个端口号
}
public void start() throws IOException {
System.out.print("启动服务端...");
//这是编写服务器常见做法 ,循环不停处理客户端发来的请求
while (true) {
//1.读取请求并解析 , packet是receive的输出型参数
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
socket.receive(packet);
//二进制转成字符串,getData可以拿到内置的字节数组
String request = new String(packet.getData(), 0, packet.getLength());
//2.根据请求计算相应 , 此处是回显服务器 , 没有计算过程
String response = process(request);
//3.把相应返回给客户端
//不用.size是因为单位是字符 , 不是字节
DatagramPacket data = new DatagramPacket(response.getBytes(), response.getBytes().length, packet.getSocketAddress());
socket.send(data);
//4.打印日志
System.out.printf("[%s:%d,request:%s,response:%s]\n",packet.getAddress().toString()
,packet.getPort(),request,response);
}
}
//根据请求计算响应
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer ser=new UdpEchoServer(8886);
ser.start();
}
}
服务器 :被动接收请求的一方
客户端 : 主动发起请求的一方
客户端要想发起请求要提前知道服务器在哪 ,
IP 那台机器上
端口号 那个程序上
要想客户端知道服务器的上述请求需要服务器把两个信息提前确定好
服务器的IP好确定 ,服务器的进程运行在哪个机器上
服务器的端口号 ,需要编写服务器的程序员指定
端口号是2字节的整数
取值范围是0-65535
只要确保你的端口不和其他进程的端口冲突即可
IP`不用设置 , 那个机上运行 , IP就是谁的
请求(request) :特指客户端给服务器发的数据
响应(response/reply)特指服务器给客户端返回的数据
receive能够读取到的前提是确实受到了客户端没有发来数据 , receive就会阻塞
传入参数表示一个UDP数据包传输的一次基本单位
packet.getSocketAddress()
UDP是无连接的
UDP协议层面上 , 是不会保存对端的信息的
因此我们们发送的时候需要把要发给谁这样的信息设置进去
此处就是要设置数据要"发给谁"
它是从前面接收的时候获取的 ,放到了前面packet数据包里面
socket这里不用关闭,因为这个对象生命周期应该是跟随整个进程的
进程结束,PCB(进程控制块)统一释放 , 不用担心"泄露"问题
客户端端口号不需要和服务器端口号一样
跑通了 , 但我们追求的是跨主机
如果服务器运行在我的电脑上 , 客户端运行在你的电脑上 , 那你们的客户端能访问我的服务器吗?
只能我们在一个局域网的时候
那咋整?
云服务器可以!厂商会提供机器,把机器租给我们使用,它具有外网 IP,在哪里都能连上
我们现在想要完善这个功能 , 输入汉语 , 输出英语
process方法改一下
一个端口号只能被一个进程关联
有没有办法查看哪些端口号被使用呢
netstat -ano 这样儿命令
Java
public class UdpDictServer extends UdpEchoServer{
Map<String,String> map=new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
map.put("hello","你好");
map.put("haha","哈哈");
}
public String process(String request){
return map.getOrDefault(request,"没有查找到");
}
public static void main(String[] args) throws IOException {
UdpDictServer udp=new UdpDictServer(3333);
udp.start();
}
}
TCP
也是两个核心类
ServerSocket 给服务器使用
就像是销售拉客的
Socket 既会给客户端用 , 也会给服务器用
针对一个客户端 , 提供具体的服务 , 销售
]对接的
没有xxxpacket这样的类表示传输的基本单位
TCP传输的基本单位 , 叫做字节(byte)
TCP有链接
具体建立连接的流程 , 是操作系统内核实现的 , 代码体现不出来
客户端new Socket对象 , 触发TCP建立连接流程 , 服务器这边不做处理 , 就能配合客户端把链接处理完
但建立好的连接需要拿到应用程序中,用accept
TCP有两种模型 ,
1.短链接 :一个连接 , 只发送一次请求响应(不常用 , 建立连接 , 断开连接都是开销的
2.长连接:一个连接 , 会进行多次请求响应
对于TCP , 端口IP就在socket里面存着 , 而UDP的不是在DatagramSocket中 , 是在DatagramPaket
java
public class TcpEchoClient {
private Socket socket=null;
public TcpEchoClient(String address,int port) throws IOException {
socket=new Socket(address,port);
}
public void start(){
System.out.println("客户端启动");
while(true){
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
Scanner scanner = new Scanner(System.in);
Scanner scanner2 = new Scanner(inputStream);
PrintWriter printWriter = new PrintWriter(outputStream);
while (true) {
System.out.println("->");
if (!scanner.hasNext()) {
break;
}
String request = scanner.next();
printWriter.write(request);
if(!scanner2.hasNext()){
break;
}
String response=scanner2.next();
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();
}
}
为什么没有反应
因为printWriter.write(request);进行IO过程 , 属于抵消操作 ,为提高效率 , 只是写入缓冲区 , 满了之后才会释放出来 , 没有真的发送
这里我们用
flush , 冲刷缓冲区 , 缓冲区没有满也发送
服务器那里也要记得加上
之前用的read , writer , fileInputaStream/fileOutStream这些都是不带缓冲区的
代码
UDP的请求响应有缓冲区吗
他的缓冲区在操作系统内核里 , 应用程序不涉及 ,
TCP在应用程序里涉及到
flush是针对应用程序这里的缓冲区进行操作的
当前代码中存在资源泄露
java
while(true){
//如果客户端和服务器建立好了连接 , 就会返回
Socket socket=serverSocket.accept();
processConnection(socket);
}
这里每次连接的时候 , 都会触发一次accept , 都会创建一个socket , 占用一个文件描述表的位置 , 在finally地方放一下
其他的生命周期跟随整个进程
代码
现在我们点击再次运行 , 会发现只是停止旧的在开启新的 ,
并不是多个客户端一起运行
启动多个客户端怎么弄
点击这个长方形

点击编辑配置

点击修改选项

允许多个实例

就欧克了
现在点击运行客户端 , 会发现运行几个就会有几个客户端 , 但只有第一个可以使用 , 为甚
我们把第一个关掉 , 第二个好了 , 第二个关了 , 第三个好了
现在看一下UDP服务器
试试UDP客户端
不会有这个情况
TCP代码有问题!!!
第一个客户端连接上服务器之后 , 会一直在connect里面打转 , 出不来 , 第二个只能卡在accept上面
建立连接了 , 但是是在内核里面
在应用程序里面如果不去调用accept , 连接没法拿到应用程序中 , 没法进一步进行这里的读写处理
就像是一个服务员多个客人 , 顾此失彼 , 我们多找几个服务员
用多线程 , 多个线程分别去处理这些逻辑
Java
public void start() throws IOException {
System.out.println("开始了");
while(true){
//如果客户端和服务器建立好了连接 , 就会返回
Socket socket=serverSocket.accept();
//processConnection(socket);
Thread thread=new Thread(()->{
try {
processConnection(socket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
好使了好使了!!!
这样做就好了!!!激不激动 , 惊不惊喜 , 意不意外!!!
好激动!和之前的知识串上了!!!
上述"多线程"模型服务器是属于多年前常见的服务器实现方式
如果客户端进一步增加 .线程数也会很多 , 线程会频繁创建释放 , 虽然轻量 , 但是开销无法忽略
用线程池合适
但是这个这个对于客户持续时间短这样是合适的 , 但是持续时间长就不合适了 , 同一时刻线程数目会很多 , 线程不是越多越好
进一步优化
IO多路复用
本质上一个线程同时处理多个socket , 一个socket不是一直需要传输数据
在等待的过程中 , 处理其他的线程
这里不做过多解释
在tcp代码中 , 有一个隐含条件 , 请求和响应都是带\n作为结尾的
读到空白符就返回 , 因此我们用println , 用print会发现不行
不能在控制台输入 \n, scanner.next()会自动把\n去掉
Tcp也可以整个字典
在日常工作中 , 大概率不会直接使用socket.api
后面会接触到更上一层的框架 , 胆汁能解决一部分问题 , 会有一部分专门问题框架不能胜任 , 就需要他了