基于tcp的C/S模型

基于TCP/ip模型下的c/s交互模型

tcp网络通信的小知识

  1.因为tcp是面向连接的,所以在写基于tcp服务器的代码时,要有listen套接字和accept套接字,而基于udp模型的代码,并且udp客户端直接调用 recvfrom/sendto 直接通信即可,不用调用connect函数,这也分别体现出了它们的特性tcp面向连接,而udp则是无需连接。
  2.对于read在网络通信中,因为tcp是基于字节流的,所以每次read上来的数据都是一个数据段。可能你这里发了一个1024字节的数据,到了运输层可能分段,所以对端可能不会一次性对上来1024字节的数据。所以由此看出来read每次读socket的文件的时候,都读的是一个数据段。
  3.对于阻塞socket而言,write调用的时候,当我们把应用层的数据拷贝到内核缓冲区的时候,如果内核缓冲区已满,那么就会阻塞,从write 函数返回也并不代表,数据发送到了对端,只是代表了数据已经拷贝到了内核缓冲区。
  4.对于UDP来说,因为是不可靠的,所以也就udp socket也就没有内核缓冲区,网络层加上包头后直接把数据发送到 数据链路层 的 队列中。从write 返回就是代表数据报已经写入到了数据链路队列。如果 我们发的应用层数据大于 SO_SNDBUF(套接字发送缓冲区上限) 会收到 EMSGSIZE 错误,这不像tcp 会阻塞。
  如果链接队列满了,因为是udp 协议栈向上报错,一直到发送方。udp 并不会重传,所以内核会向应用层返回一个 ENOBUFS 。
  5.内核的TCP发送缓冲区会一直缓存从应用层拷贝到的数据,一直到收到ACK后,才把这些数据丢弃。
  6.write成功返回,对于 TCP来讲只是应用层数据被拷贝到了内核发送缓冲区中,对于 UDP来讲只是数据被加入到了 数据链路层队列中。
  7.connect调用失败后,并不可以直接重用该socket,重用前需 close再用,因为基于 Tcp状态转换图得知 , connect 函数其实就是三次握手,那么当三次握手失败的话,socket 不是处于 CLOSED 而是SYN_SENT状态,所以我们必须重新关闭才能再次使用该 socket。
  8.对于sockaddr 结构体的疑问,为什么这些socket api 不直接使用void* , 而使用这个通用结构体,因为socket api 比 c 语言的 void * 更早出世,所以在当时没有 void* 这一概念的时候就使用了通用结构体来充当void*。

tcp数据流

这里写图片描述
1. 应用层把数据发送给TCP.
2. TCP根据 MSS大小把数据分为一个个的数据段(SO_SNDBUF是发送缓冲区大小)
3. IP层加上ip 头部后转发给相对应的数据链路层(可能这里会分片)
4. 数据链路层有输出队列,如果输出队列满了,逆向数据流向上发送错误,收到错误后TCP会过段时间再发一次数据。这些情况都是内核实现的,应用根本不知道传送的情况。

tcp_server

首先我们需要先有个套接字,这个套接字必须绑定服务器相应的ip地址和port端口号。而且这个套接字需要是listen状态的。那么当有client向tcp发送连接时,服务器进程调用accept函数就可以查看listen的未决连接队列是否有未处理的连接,如果有就创建一个相应cilent连接的套接字返回,这时我们就可以通过这个accept套接字和cilent通信了。

步骤
socket调用
bind 绑定服务器ip和端口号
listen 使该套接字成为监听状态
accept 调用拿到与相应cilent绑定的套接字与之通信。

tcp_cilent

cilent的原理就很简单了,首先调用socket 申请个套接字,然后调用connect连接server的ip/port,connect返回的这个套接字我们就可以与server通信了。

模型中使用的函数

int socket(int domain,int type,int protocl)

第一个参数 指网络层是什么类型的协议, ipv4 ipv6 之一类。
第二个参数 指传输的数据流的类型,tcp 为 sock_stream (udp为sock_DGRAM,可见udp传输的是数据报,不可靠传输)
第三个参数 基于1/2参数选项组合来选的,一般设置为0,让内核为我们选择

int bind(int sockfd,const struct * sockaddr,socklen_t addrlen)

第一个参数为 需要绑定的套接字
第二个参数为 关于socket地址的数据结构,它其中记录了套接字要使用到的ip/port,所以定义出这个结构体就可以跟我们申请的套接字绑定了。(一般网络协议不同套接字地址结构体不同,调用时需要去强转)
第三个参数为 第二个参数类型的大小。
它的返回值很特殊,成功返回0。

bind中的IP地址

  对于Client端来说,如果我们绑定了IP表明,这个IP是它的源IP。对于Server端来讲,绑定了IP表明 Server只能接受这个IP上的连接(也就是固定网卡接口了)。
  如果我们不自己设置,内核也会为我们设置。让内核来选择随机端口的话,我们只需设置端口号为0即可。对于让内核来选IP地址则,如果是IPV4给其赋值 INADDR_ANY 即可,也就是0。对于IPV6,则需要赋值 in6addr_any 这个是结构体,不过它值也是0,但是我们不能直接用0赋值。
  内核选择IP地址的时机是当有一个连接Connect(TCP)也就是三次握手完毕后或者 当一个UDP数据报被 发出去(UDP), 此时内核才会为socket绑定地址。
  选择IP地址的方式对于Tcp来说,如果是一个Server(listen态socket),内核是这样的,根据Client端发来的 SYN段中的目的端地址作为 源IP。如果是一个Client,内核会根据要连接的server的路由情况,从各个网卡中选择一个合适的IP地址。

bind中的端口号 (第二个参数为const的缘故不能直接查看ip与port)

  当我们设置端口号为0的时候,内核会为我们选择一个随机端口号,由于第二个参数是const的原因,所以调用完毕后,我们无法查看内核为我们选择的端口号,我们只能通过getsocketname来查看。
  绑定知名端口号需要root权限。

bind绑定相同的地址

  有时我们不想Server主动断开连接后由于time_wait,而不能立即重启,所以就想重新绑定相同的地址的Server。
  那么为了端口复用需满足以下条件其中之一:

  1. socket绑定的不是同一网卡可以绑定
  2. 设置了 SO_REUSEADDR并且不能是LINTEN状态的节点
    可见如果是处于time_wait的节点,我们只需提前设置 SO_REUSEADDR即可端口复用,解决Server不能立刻重启。
将ip 与 port 设置为0

  当我们设置0,并非内核中对应的连接节点的ip与port就是0,只是代表让内核帮我们选择合适的 ip 与 port 来绑定。

int listen (int sockfd,int backlog)

第一个参数 绑定后的套接字
第二个参数 listen_socket 的 有俩个队列。完全链接队列(状态为ESTABLEISHED)与 半完全链接队列。backlog 参数是用来设置完全连接队列大小的参数,如果设置为0 ,根据不同平台其完全连接队列的值不一样,所以不想监听关闭该socket即可。
历史上backlog参数是设置这完全与非完全连接队列的大小,但是由于黑客的Syn攻击,导致非法连接占满了完全连接队列,导致正常客户的请求无法连接进来

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值