TCP客户服务器编程模型
文章目录
- TCP客户服务器编程模型
-
- 一、前言
- 二、TCP客户服务器模型
-
- [2.1 模型](#2.1 模型)
- [2.2 UDP的服务端和客户端](#2.2 UDP的服务端和客户端)
-
- [2.2.1 特点](#2.2.1 特点)
- [2.2.2 数据报服务](#2.2.2 数据报服务)
- [2.3 TCP客户、服务器模型](#2.3 TCP客户、服务器模型)
-
- [2.3.1 特点](#2.3.1 特点)
- [2.3.2 数据流服务](#2.3.2 数据流服务)
- [2.3.3 具体细节解析](#2.3.3 具体细节解析)
- [2.3.4 代码分析](#2.3.4 代码分析)
- 三、小结
一、前言
今天来讲关于传输层一个重要的编程模型------TCP客户服务器编程模型。它和先前的UDP有很多相似与不同之处呢~
二、TCP客户服务器模型
2.1 模型
应用程序往往包含一个服务端和很多很多客户端,客户端和服务端进行连接,进行数据的发送和响应。
2.2 UDP的服务端和客户端
2.2.1 特点
不可靠传输,UDP服务端只维护了端口 和应用程序进程号之间的关系
UDP
A、B之间只有一个buffer,B向A发送2个包,直接丢到buffer里面,C也向A发数据包,也丢到buffer里面,但是顺序保证不了,可能B的第一个包到了,第2个还没到,但是C的包先到了,A来取数据的时候,发现了B1、C1、B2。处理数据就不能保证有序了。并且在传输的过程中,丢包就丢包了,服务器根本不care。
2.2.2 数据报服务
这里的buffer本身也是queue,但是这里建立的连接是服务端能跟所有的客户端都能建立 ,而TCP 是保证服务器必须跟某一个建立独立的唯一的管道。
2.3 TCP客户、服务器模型
2.3.1 特点
在端口和应用程序进程号之间关系基础上,又实现了协议头内保证可靠传输
什么是可靠传输呢 ?
就是要通信两端实现发送确认机制
UDP能不能实现可靠呢?
当UDP服务端开放了一个端口,UDP的客户端有很多很多,且都可以往服务端这个端口发信息。
需不需要每个客户端和服务端要分别建立确认机制呢?UDP的协议头没有包含任何字段的。UDP客户服务模型是数据报服务,也就是说在传输层上没有办法实现数据的确认机制,只能收到一个个数据包,但是不能回。
2.3.2 数据流服务
TCP是数据流服务 ,这是借助了管道思想 (Linux中也有这种进程和进程之间的通信方式),也是数据结构中的队列 的一个典型应用。A发送的数据,最终不是发给B,而是发给B的内核(传输层)里的一个队列的数据结构。
队列具有先进先出的特点,本身就是个buffer,不能随机访问。
数据流服务就是在A和B之间建立一个队列,不是在一个机器上建立,而是借助传输层的通信协议,A和B利用一根管道,保证数据无丢失的传输。B的缓存区将A的数据逐个放到队列里,B有空的时候再接收。
这个结构具有2种行为:缓存行为、顺序行为(也是队列天然的特点)。
到这里,你应该能够感受到TCP需要建立连接(发送确认),UDP不需要建立连接,因为它没有发送确认的缓存。
2.3.3 具体细节解析

从上图我们可以看出很多重要的信息,你会发现每一条连接都会有一个发送队列 和一个接收队列。
UDP的Foreign Address是不需要知道的,因为这之间不需要建立连接,只需要往里面丢数据就可以了。而TCP中会有状态(state)的概念。这个状态到底是怎么一回事呢?
服务器打开了6666的端口,这个"门"只能打开一次。但是在TCP中就会出现一个问题:到底是什么"魔法",使得A和B与C都建立了连接,B和C为什么都能和A在同一端口进行传输?

从图中,我们可以看见3个22,难道我们开了3个端口号吗?
当然不是!
TCP中,每一条TCP的连接都有一个状态(state),状态决定了每一个socket对象的内部行为。
- 监听的链接(state = LISTEN)
- 传统的链接:传统的socket(抽象),把内核中复杂的一个东西抽象成一个整数来描述(state = ESTABLISHED)
UDP:内核开放的一个数据结构,这个数据结构里面包括很多信息,比如:我是谁。。。你可以向我发消息
用3这个文件描述符来指向这个数据结构(以后应用程序访问3就能访问这个数据结构)
文件描述符只是一个抽象,它实际指向里面很多具体的信息。网络上的连接:src: ip: port; dest: ip: port
服务器的回应的数据包是由传输层内核的代码,直接就发送完了(TCP自动做的)。这个发送是基于知道"我是谁,你是谁"(src,dest),才能建立关系,发送消息。
在TCP中,仅用一个buffer就不太可能实现了。

如何维护好这种传输呢?
为了能实现每一个客户端都独享这个发送确认机制,产生了TCP的分发机制。TCP会有一个监听状态(类似于公司的前台),前台没有任何业务能力(不能处理普通数据,普通数据要发送确认机制),就只会处理状态数据,因此会有监听状态这个描述符。

A发起链接,前台不会管这个服务,只会申请出一个客户经理,客户经理专门关心A和B的关系,客户经理负责建立连接。
前台针对每个链接分出不同的独立的客户经理,这些客户经理都是共享src的**,这就是为什么src的端口号是一样的,但是dest是不一样的**,这就形成了不同的链路,每条链路都对应一个数据结构来实现发送确认机制,都需要文件描述符实现指向。
注意:上述的客户经理和前台等都是指服务端的指代。
2.3.4 代码分析
完整代码:
c
#includestdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<sys/types.h>
#include<arpa/inet.h>
// TCP服务器
int main()
{
struct sockaddr_in self; // 结构体里包含了IP和端口信息
int ret;
// 获取一张信封,传输层采用了TCP技术
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if(tcp_socket == -1)
{
perror("socket");
return -1;
}
// 将这个信封绑定系统的一个端口号,其他客户端就能通过这个端口号向你发出信息
// 服务器就可以通过这个端口号来获取信息(而是客户端连接的状态)了
memset(&self, 0, sizeof(self));
self.sin_family = AF_INET;
self.sin_port = htons(6666);
self.sin_addr.s_addr = inet_addr("xxx.xxx.xxx.xxx"); // 添自己的主机号
ret = bind(tcp_socket, (const struct sockaddr*)&self, sizeof(self));
if(ret == -1)
{
perror("bind");
return -1;
}
// 默认socket都是具有双向能力(socket既能发送也能接收),TCP需要一个被动监听的描述符来实现新链接分发(3次握手)
// 默认的socket的状态,切换成被动监听的状态
listen(tcp_socket, 5); // 代表TCP服务器最大有5条链接(建立3次握手的链接)
printf("listening...\n");
// getchar();
char buf[128];
size_t len;
// 系统提了一个叫做accept函数,来监听描述符的消息,3次握手已经成功的消息【新的客户经理】)
while(1) // 服务器肯定不能死机
{
// 应用层需要循环等待新链接
int new_fd = accept(tcp_socket, NULL, NULL);
if(new_fd < 0)
{
perror("accept");
break;
}
printf("have a new connection!\n");
// 接收客户端发来的消息
recv(new_fd, buf, sizeof(buf), 0);
len = recv(new_fd, buf, sizeof(buf), 0);
if(len <= 0)
{
perror("recv");
break;
}
buf[len] = 0; // 结束标志
printf("client: %s\n", buf); // 必须要有\n,如果没有,其实执行了,但是在缓存中
close(new_fd);
}
close(tcp_socket);
return 0;
}
可自行利用上述代码,在不同的地方加打印信息以及
getchar();进行调试,以便更深入的理解TCP的客户服务模型。Q&A
如果出现没有显示端口的情况,可能是防火墙将端口给阻断了,不能打开端口。可以切换为root用户,修改一下防火墙的配置。
Linux// 配置指令 firewall-cmd --list-all firewall-cmd --add-port=6666/tcp --permanent firewall-cmd --reload
c
listen(tcp_socket, 5); // 代表TCP服务器最大有5条链接(建立3次握手的链接)
A向前台发送数据,前台要确定A没问题才会分离出客户经理来,在客户经理来之前,前台要和B保持沟通,确定A是否是想要和前台建立连接。这个过程就是3次握手。
TCP为了能跟不同的人进行3次握手,需要一个缓存。在代码中就是指5条
B向前台发送请求连接,前台收到并回一个信号,表明自己可以为它分配一个客户经理,如果这个回的包没有收到,B不知道能不能连,会有一个超时重传 ,经过一段时间之后,前台可以再次发送。为什么能这样呢?说明会有一个缓存,可以使它再次发一次。但是缓存是有限的。(否则会导致服务端有大量资源占据但没有被利用)
重传机制,这也是可靠传输的生动体现~

这就表示前台已经申请出来了。
c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<sys/types.h>
#include<arpa/inet.h>
// TCP服务器
int main()
{
struct sockaddr_in self; // 结构体里包含了IP和端口信息
int ret;
// 获取一张信封,传输层采用了TCP技术
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
if(tcp_socket == -1)
{
perror("socket");
return -1;
}
// 将这个信封绑定系统的一个端口号,其他客户端就能通过这个端口号向你发出信息
// 服务器就可以通过这个端口号来获取信息(而是客户端连接的状态)了
memset(&self, 0, sizeof(self));
self.sin_family = AF_INET;
self.sin_port = htons(6666);
self.sin_addr.s_addr = inet_addr("192.168.233.129");
ret = bind(tcp_socket, (const struct sockaddr*)&self, sizeof(self));
if(ret == -1)
{
perror("bind");
return -1;
}
// 默认socket都是具有双向能力,TCP需要一个被动监听的描述符来实现新链接分发(3次握手)
// 默认的socket的状态,切换成被动监听的状态
listen(tcp_socket, 5); // 代表TCP服务器最大有5条链接(建立3次握手的链接)
printf("listening...\n");
getchar();
return 0;
}
当应用程序阻塞了,不会做任何代码执行了,此时客户端向服务器发起连接,这个时候客户端请求连接能成功吗?

结果显示能成功建立连接。这说明:建立连接的协议,是传输层在做,应用层只是使用者。


能够发送信息。tcp的recv-Q可以接收数据。
但是在这里出现一个问题。
先前我们说UDP,发送数据包,在缓存(recv-Q)中,因为UDP是数据报传输,数据在里面存储是以等长包的形式。因此不会发生数据粘包。
但是,在TCP中,数据流传输,就是一个char buf[1024],直接就是一传一取,直接把所有的东西都取完了,这就是TCP粘包。
TCP粘包;UDP只是丢包,不会粘包
发送端发送时本来就不是一次性发完的,本来就是拆成小包来发,最后一收,本来就该黏在一起。如果是聊天的话,还是有专门的协议来规定的。
3次握手的目的:建立服务端和客户端之间的可靠连接
如果没有3次握手,客户经理就不会出来。
客户经理出来,应用层得知道它的存在,不然怎么给它发消息呢?
因此监听描述符中应该也有一个缓存区,这个缓存区是专门存已经成功连接的客户,然后让我们使用一个接口函数把这个客户的信息给取出来。
系统函数来等待监听描述符对应的缓存里有没有已经链接成功的客户端经理(这里的客户端经理也就是文件描述符)
类比
scanf,scanf从标准输入中获取消息,如果标准输入的缓存里没有数据,那么这个函数等待(阻塞)。只有敲了回车,才能返回输入的信息。

在这里程序只能接收一次关闭。

把注释取消掉,我们在这里抓一下包。

应用层getchar()停在那里,仅发生了发生了3次握手
当在客户端发送"hello"时:

可以看出3次握手之后,就只有发送接收两次了

仍然没有收到(阻塞中。。。),回车之后:

粘包了。
关于TCP还有很多内容,我们后续再探讨。。。
三、小结
相信你对TCP服务客户模型有了一定的了解,当然其被背后还有更深层的东西亟待探索。之后我们将先深入探索关于传输层的相关协议。


