基于 TCP 与 UDP 的服务器器端 / 客户端程序开发
基于 TCP 的 C/S 程序 - 服务器器端分析 Client Server
listen() 的 工作过程 调 用 listen() 函数进 入等待连接请求状态 在 listen() 之后, 客户端的 connect() 调 用才有作 用 listen(int sock, int backlog) 成功返回 0, 失败返回 -1
listen(): 连接请求等待队列列 客户端请求连接时, 服务器器受理理连接前,( 客户端 ) 连接 一直处于等待状态
listen(int sock, int backlog) 函数两个参数的形象 比喻 sock: 门卫 大 门 传达室 客户请求建 立连接时, 先咨询 门卫 : 是否可以建 立 连接?
listen(int sock, int backlog) 函数两个参数的形象 比喻 backlog: 等待室 休息室 门卫同意 用户连接后, 用户进 入后需要在休息室等 待 休息室的容量量是有限的
再谈 backlog 参数 根据服务器器端的特性确定 例例如, 请求频繁的 Web 服务器器可设为 15 如何确定 : 始终是按照实验来确定
accept() 的 工作过程 队列列中等待的请求会被服务器器最终处理理 处理理时由哪个套接字来处理理?
int accept(int sock, struct sockaddr * addr, socklen_t * addrlen) sock: 服务器器套接字的 文件描述符 (fd) addr: 保存发起连接请求的客户端的地址信息 addrlen: 第 二个参数的 长度 为何第 二 三个参数传指针?
accept() 工作过程 受理理连接请求等待队列列 里里的客户端连接请求 若函数调 用成功, 则函数内部产 生 用于与客户端通 信 (I/O) 的套接字, 返回其 文件描述符 (fd) 注意 : 套接字是 自动创建的, 与服务器器的套接字并 不不同
代码分析 :Hello socket Server
基于 TCP 的 C/S 程序 - 客户端分析 Client Server
connect() 客户端与服务器器端的显著区别 : 服务器器端 等待连接 客户端 请求连接 服务器器端调 用 listen() 之后, 客户端就可以请求了了
int connect(int sock, struct sockaddr * addr, socklen_t * addrlen) sock: 服务器器套接字的 文件描述符 (fd) addr: 保存可建 立起连接的服务器器端的地址信息 addrlen: 第 二个参数的 长度
调 用 connect 函数以后, 客户端将 一直等待返回结 果, 直到下 面情况的发 生 服务器器端接收连接, 客户端获得返回结果 发 生异常, 中断连接请求
connect() 调 用后的数据交换 服务器器端的 listen() 函数需要 一直等待客户端, connect() 也需要等待服务器器端接收连接请求 他 们的等待有何联系和先后次序? connect 调 用后不不必 马上交换数据
客户端的套接字地址信息在哪 里里? 一个 TCP/IP 应 用层程序需要 至少对应 一个 <IP 地址, 端 口号 > 服务器器端运 行行时, 知道 自 己的 <IP 地址, 端 口号 >, 显示地 用 bind 来绑定到本机 客户端没有 bind 其 IP 地址和端 口号在 connect 函数处确定 本机 IP 地址 随机分配端 口号
代码分析 :Hello socket Client
总结 : 基于 TCP 的 C/S 程序 函数调 用关系
总结 : 基于 TCP 的 C/S 程序 函数调 用关系 客户端的 connect() 必须要在 listen() 之后才可以调 用 调 用后等待 accept()
服务器器端的 listen() 和 accept() 调 用listen() 后开始接收客户端请求 accept() 的调 用与客户端请求没有严格的顺序要求 先来请求, 再 accept 直接先 accept? 再来请求也可以
迭代执 行行 : 更更实 用的 C/S 程序 已经讨论的程序将每个函数执 行行 一遍,close() 了了 socket 以后就退出运 行行了了 实际的持续提供服务的服务器器不不可能执 行行 一次程序就 关闭了了 需要有持续运 行行的基于 TCP 的 C/S 程序 用循环实现即可
迭代的回声服务器器 回声 (echo) 服务器器 定义 满 足如下 ( 比较简化的 ) 设计要求 : 服务器器端同 一时刻只处理理 一个客户端请求 服务器器端依次向五个客户端提供服务, 然后退出 客户端发送 文本给服务器器, 服务器器回传 (echo back) 客户端输 入退出指令, 结束 一个客户端的请求
Server
Server
Server
Client
跳出循环, 结束 socket, 导致 Server 端的 50-51 行行 while 中断 Client
回声程序存在的问题思考 Client
回声程序存在的问题 - 如何解决? 策略略 一 : 提前如果知道传递的数据 长度 策略略 二 : 约定 一个分界符
TCP 原理理 : 回顾 TCP 套接字的 I/O 缓存 ( 缓冲 ) 机制 对于同样 一段发送和接受的数据, 发送过程和接受过程对于数据的操作次数是可以不不 一样的, 例例如 一个 长 55 字节的数据 : 可以传输 11 次, 每次传输 5 个字节 可以接收 5 次, 每次接收 11 个字节 可以传输 / 接收 10 次, 传输的字节数 1+2+...+9+10
TCP 套接字的 I/O 缓存 ( 缓冲 ) 机制续 接收端来不不及处理理的数据都放在哪? 发送端缓存 接收端缓存 read/write 函数只负责在调 用的瞬间把数据读出 缓存 / 加 入缓存
TCP 套接字的 I/O 缓存 ( 缓冲 ) 机制续 输 入缓存会不不会 爆仓? TCP 会控制数据流, 不不允许发 生超过输 入缓冲 大 小 的数据传输 滑动窗 口 (Sliding window) 协议,
TCP 套接字的 I/O 缓存 ( 缓冲 ) 机制续 滑动窗 口协议
TCP 套接字的 I/O 缓存 ( 缓冲 ) 机制续 套接字 A: 你好, 最多可以向我传递 50 字节 套接字 B: OK ( 发送 小于 50 字节 ) 套接字 A: 我腾出了了 20 字节的空间, 最多可以收 70 字节 套接字 B: OK
TCP 的 I/O 过程
基于 UDP 的 C/S 程序 TCP 与 UDP 的最主要区别 可靠性 流控制
UDP 的内部 工作原理理 UDP 的最重要作 用就是根据端 口号来将数据包交付给 应 用层程序
UDP 的性能 之前的 比喻 : TCP 是流 水线 /UDP 是快递 TCP 是电话 /UDP 是邮件 TCP 比 UDP 快? UDP 是 TCP 的 一个上界
UDP 的性能续 实际上 UDP 并 非每次都 比 TCP 传输快, 原因可能为 收发数据前后进 行行的连接设置与清除 UDP 参与者两端为了了保证可靠性 而添加的流控制
基于 UDP 的 C/S 程序 在基于 TCP 的 C/S 程序上理理解 UDP UDP 中服务器器端和客户端没有连接 无需经过连接过程 :listen() accept() UDP 程序只有创建套接字的过程和数据交换的过程 不不分服务器器端和客户端
UDP 服务器器 / 客户端的套接字 TCP UDP 的服务器器端和客户端均只需要 一个套接字 与 TCP 的 门卫 模式对 比?
UDP UDP 为 邮筒 模式 只需要 一个套接字即可 用来和多个主机通信 套接字是被 复 用 的
UDP 的 I/O 函数 基于 TCP 的传输不不需要每次都指定对 方地址信息, 因 为连接是保持的,TCP 套接字知道对 方地址信息 UDP 每次传输时都要传 入 目标地址的信息 每次写信都要写地址 邮筒不不包含 目标地址信息
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addr_len); buf: 待传输数据缓冲的地址 nbytes: 待传输数据 长度 to: 目标地址信息的指针
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addr_len); buf: 待传输数据缓冲的地址 nbytes: 待传输数据 长度 to: 目标地址信息的指针
基于 UDP 的 C/S 程序 - 代码分析 服务器器端 Server
Server
客户端 Client
Client
看个栗栗 子
回顾 : 流套接字通 用编程模型 服务端 : 套接字的创建 绑定套接字到指定的 IP 地址和端 口号 设置套接字进 入监听状态 接收连接请求 收发数据 客户端 : 套接字创建 申请建 立连接 收发数据 断开连接 关闭套接字 关闭套接字
Socket ( ) s bi nd( ) s l i st en( ) TCP accept ( ) accept ( ) r ecv( ) / send( ) / cl osesocket ( ) Socket ( ) s Connect ( ) send( ) / r ecv( ) /
回顾 : 数据报套接字通 用编程模型 服务端 : 创建套接字 绑定 IP 地址和端 口 收发数据 客户端 : 创建套接字 收发数据 关闭套接字 关闭套接字
UDP 传输的 I/O 特性 回顾 :TCP 不不存在数据边界,I/O 是基于流的模式 TCP 传输中调 用 I/O 函数的次数 无意义 UDP 是存在数据边界的 调 用多少次输 入函数则必须调 用多少次输出函数来 配套
其中 一端 ( 服务器器端 )
另 一端 ( 客户端 )
TCP 和 UDP 的 bind() 函数问题 bind: 把 IP 和端 口信息绑定到本机的 socket bind 的意义 : 告诉操作系统该侦听哪个 IP 和哪个端 口 bind 以后, 该 IP 和该端 口的消息都会被收到并处理理 bind 的两个功能 : 绑定 IP 绑定端 口
bind 函数对服务器器端的意义 由于服务器器端不不是发起连接的 一端, 而是等待客户端 来连接 因此服务器器需要有 一个明显的 目标 被客户端 找到 用 bind 来固定服务器器端的 IP 地址和 一个端 口 客户端可以找到
bind 之于客户端 在 TCP 里里, 客户端的端 口号是服务器器端给分配的 没有必要 bind 如果 bind 反 而只限定了了某端 口和服务器器进 行行通信 在 UDP 里里, 客户端只需知道服务器器端的地址和端 口 若完全对等通信, 不不分 C/S, 双 方都要 bind
bind 的简单归纳 : 无论服务器器或客户端 1. 需要在建 立连接前就知道端 口的话, 需要 bind 2. 需要通过指定的端 口来通讯的话, 需要 bind
客户端的 IP 地址与端 口分配 服务器器端, 通过 bind 可以绑定本地服务器器端 socket 的 IP 地址和端 口 客户端如果指定 用于 socket 通信的 IP 地址和端 口? TCP 中, 调 用 connect 函数的时候, 如果服务器器端 运 行行 accept 了了, 则 自动给客户端分配 一个端 口号 用于通信 UDP 中, 在第 一次调 用 sendto 函数时分配
TCP 半关闭 : 优雅地断开套接字 TCP 中断开连接 比建 立连接过程更更加关键 连接过程 一般不不会出现太 大问题 断开连接的过程可能会较复杂, 涉及到两个防线数 据传输的断开 调 用 close() 函数直接断开 TCP 连接, 显得不不够优雅? close() 直接将所有数据传输都断掉
令主机 A 调 用 close( 或者 winsock 下的 closesocket) 之后主机 A 无法调 用任何发送与接收相关的函数 主机 A 不不能再向 B 传送数据, 同时主机 B 可能还有给主 机 A 传数据的需求
解决 方案 只关闭 一部分 (Half-close) 数据交换中使 用的流 可以传输数据, 但 无法接收, 或, 可以接收数据, 但 无法传输 半双 工?
shutdown() 函数 int shutdown(int sock, int howto); 成功时返回 0, 失败时返回 -1 sock 需要断开的套接字 文件描述符 (fd) 断开的 方式
shutdown() 之前提到过, 可以断开 一边 ( 传出或传 入 ) 通过传递第 二个参数 howto 来实现 SHUT_RD: 断开输 入流 SHUT_WR: 断开输出流 SHUT_RDWR: 同时断开 I/O 流
SHUT_RD 断开输 入流, 套接字 无法接收数据 及时输 入缓冲收到数据, 也会被抹去 SHUT_WR 断开输出流, 无法传递数据 但是, 若输出缓存还有没发完的数据, 则继续发送 SHUT_RDWR 相当于调 用两次 shutdown, 一次参数为 SHUT_RD, 一次参数为 SHUT_WR
为何要 优雅地 断开连接 为什什么要有半关闭 (half-close) 因为 一 方结束传输后另 一 方可能还需要保持数据传输 可以等待 足够 长的时间等到完成数据交换即可? 但这 里里 足够 长 如何把握
两个考虑 : 客户端必须等到服务器器端的数据传送完毕才能处理理 与回复 服务器器端需要等待客户端数据发送完成
半关闭和四次挥 手 之间可能有数据
半关闭可能的应 用场景 尽快释放 socket 资源 : 一端的主机可能运 行行多线程程序, 某些线程需要尽快释放 socket 以便便充分利利 用 网络资源 Socket 的安全 : 多个线程可能潜在地都能访问某个 socket, 若线程不不希望 socket 被访问到, 在操作完后可单 方 面关闭 socket Socket 传输的查错 : 在 大量量数据传输过程中, 如果 网络出错导致数据 无法发出 而传送 方不不知道的话, 可以单 方 面调 用 shutdown, 该函数只有在数据都发送完成时才能返回成功值 0
半关闭代码分析 - 服务器器端 Server
Server
Server
半关闭代码分析 - 客户端 Client