Linux TCP编程

TCP/IP协议栈

根据传输方式不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字。因为TCP套接字是 面向连接 的,因此又称 基于流stream)的套接字。TCP是Transmission Control Protocol传输控制协议)的简写,意为“对数据传输过程的控制”。

TCP/IP协议栈

第一层 次 :链路层

链路层是物理链接领域的标准化结果,也是最基本的领域,专门定义了LAN,WAN,MAN等网络标准。若两台主机通过网络交换数据,则需下图所示的物理链接,链路层就负责这些。

第二层次:IP层

准备好物理链接之后就要传输数据。为了在复杂的网络环境中传输数据,首先需要考虑路径的选择。向目标传输数据需要经过哪条路径?解决该问题的就是IP层。该层用的协议就是IP

IP面向消息的不可靠的协议,每次传输数据是会帮我们选择路径,但并不一致。如果传输中发生路径错误,则选择其他路径;但如果发生数据丢失或错误,则无法解决。换言之,IP协议是无法应对数据错误的。因此,又要放下一层。

第三层次:TCP/UDP层

IP层解决数据传输中的路径选择问题,只需照此路径传输数据即可。TCPUDPIP提供的路径信息为基础完成实际的数据传输,故该层又称传输层Transport)。

IP层只关注**1个数据包(数据传输的基本单位)**的传输过程。因此即使传输多个数据包,每个数据包也是有IP层实际传输的,也就是说传输顺序和传输本身是不可靠的。若只是利用IP层传输数据,则有可能后传输的数据包B比先传输的数据包A提前到达。另外,传输的数据包A、B、C中可能只收到A和C,甚至收到的C可能已经损毁。

若添加TCP协议则按照下图的对话方式进行数据传输

第四层次 应用层

前三个层次,套接字处理过程中都是自动处理的。为了使“程序员从这些细节中解放出来”。选择数据传输路径、数据确认过程都被隐藏到套接字内部。前三个层次是为了给应用层提供服务的。

TCP 服务端

TCP 服务端代码实现

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>void tcp_server() {int server_sock, client_sock;struct sockaddr_in server_addr, client_addr;socklen_t client_addr_len;const char *message = "Hello Word!\n";server_sock = socket(PF_INET, SOCK_STREAM, 0);//TCP 协议if (server_sock < 0) {std::cout << "create socket failed!" << std::endl;return;}memset(&server_sock, 0, sizeof(server_sock));//清零server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");server_addr.sin_port = htons(9020);//(struct sockaddr *)&server_addr 为了兼容C语言 不能做成重载int ret = bind(server_sock, (struct sockaddr *) &server_addr, sizeof(server_addr));if (ret == -1) {std::cout << "bind failed!" << std::endl;close(server_sock);return;}ret = listen(server_sock, 3);//进入等待连接请求状态 成功时返回0,失败时返回-1if (ret == -1) {std::cout << "listen failed!" << std::endl;close(server_sock);return;}//受理客户端连接请求 成功时返回创建的套接字文件描述符,失败时返回-1client_sock = accept(server_sock, (struct sockaddr *) &client_addr, &client_addr_len);if(client_sock==-1){std::cout << "accept failed!" << std::endl;close(server_sock);return;}ssize_t write_len_message = write(client_sock, message, strlen(message));if(write_len_message!= strlen(message)){std::cout << "write failed!" << std::endl;close(server_sock);return;}close(client_sock);//可以省略,因为服务端关闭的时候客户端会自动关闭close(server_sock);
}

TCP 客户端

connect()函数

#include <sys/socket.h>
/**
*成功时返回0,失败是返回-1
* __fd:客户端套接字连接文件描述符
* __CONST_SOCKADDR_ARG=const struct sockaddr* : 保存目标服务器端地址信息的地址变量
* __len :以字节为单位传递已传递给第二个结构体参数__addr的地址变量长度
*/
int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)

客户端套接字地址信息在哪儿?

实现服务端毕竟过程之一就是给套接字分配IP和端口号。而客户端实现过程套接字地址分配是在调用 connect函数时、在操作系统中(更准确的说是在内核中)、IP用计算机主机IP,端口号随机分配。

客户端的IP地址和端口在调用connect函数时自动分配,无需调用标记bind函数进行分配。

这就是客户端与服务端的不同。

客户端代码

基于TCP服务端与客户端的函数调用关系

客户端与服务端联调代码实现

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>void tcp_client_01() {//创建一个子进程__pid_t pid = fork();if (pid == 0) {sleep(2);//子进程休眠2秒钟,//开启客户端int client = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in server_addr{};memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");server_addr.sin_family = AF_INET;server_addr.sin_port = htons(9527);int ret = connect(client, (struct sockaddr *) &server_addr, sizeof(server_addr));if (ret == 0) {printf("%s(%d):%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__);char buff[256] = "";read(client, buff, sizeof(buff));std::cout << buff << std::endl;} else{printf("%s(%d):%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__);}close(client);std::cout << "client done" << std::endl;} else if (pid > 0) {tcp_server_01();int status = 0;wait(&status);} else {std::cout << "fork failed!" << std::endl;}
}

迭代服务器实现

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>void tcp_server_01() {int server_sock, client_sock;struct sockaddr_in server_addr{}, client_addr{};socklen_t client_addr_len;
//    const char *message = "Hello Word!\n";server_sock = socket(PF_INET, SOCK_STREAM, 0);//TCP 协议if (server_sock < 0) {std::cout << "create socket failed!" << std::endl;return;}memset(&server_addr, 0, sizeof(server_addr));//清零server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");server_addr.sin_port = htons(9527);int ret = bind(server_sock, (struct sockaddr *) &server_addr, sizeof(server_addr));if (ret == -1) {std::cout << "bind failed!" << std::endl;close(server_sock);return;}ret = listen(server_sock, 3);if (ret == -1) {std::cout << "listen failed!" << std::endl;close(server_sock);return;}printf("%s(%d):%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__);char buff[1024] = "";while (true) {memset(&buff, 0, sizeof(buff));client_sock = accept(server_sock, (struct sockaddr *) &client_addr, &client_addr_len);if (client_sock == -1) {std::cout << "accept failed!" << std::endl;close(server_sock);return;}read(client_sock, buff, sizeof(buff));ssize_t write_len_message = write(client_sock, buff, strlen(buff));if (write_len_message != strlen(buff)) {std::cout << "write failed!" << std::endl;close(server_sock);return;}close(client_sock);//可以省略,因为服务端关闭的时候客户端会自动关闭}close(server_sock);
}void tcp_client_01() {//创建一个子进程__pid_t pid = fork();if (pid == 0) {sleep(2);//子进程休眠2秒钟,//开启客户端int client = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in server_addr{};memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");server_addr.sin_family = AF_INET;server_addr.sin_port = htons(9527);int ret = connect(client, (struct sockaddr *) &server_addr, sizeof(server_addr));if (ret == 0) {printf("%s(%d):%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__);char buff[256] = "hello ,here is client!";write(client, buff, strlen(buff));memset(&buff, 0, sizeof(buff));read(client, buff, sizeof(buff));std::cout << buff << std::endl;} else {printf("%s(%d):%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__);}close(client);std::cout << "client done" << std::endl;} else if (pid > 0) {tcp_server_01();int status = 0;wait(&status);} else {std::cout << "fork failed!" << std::endl;}
}int main() {
//    std::cout << "Hello, World!" << std::endl;tcp_client_01();return 0;
}

回声服务器实现

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>void tcp_server_02() {int server_sock, client_sock;struct sockaddr_in server_addr{}, client_addr{};socklen_t client_addr_len;
//    const char *message = "Hello Word!\n";server_sock = socket(PF_INET, SOCK_STREAM, 0);//TCP 协议if (server_sock < 0) {std::cout << "create socket failed!" << std::endl;return;}memset(&server_addr, 0, sizeof(server_addr));//清零server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");server_addr.sin_port = htons(9527);int ret = bind(server_sock, (struct sockaddr *) &server_addr, sizeof(server_addr));if (ret == -1) {std::cout << "bind failed!" << std::endl;close(server_sock);return;}ret = listen(server_sock, 3);if (ret == -1) {std::cout << "listen failed!" << std::endl;close(server_sock);return;}printf("%s(%d):%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__);char buff[1024] = "";for (int i = 0; i < 2; ++i) {memset(&buff, 0, sizeof(buff));client_sock = accept(server_sock, (struct sockaddr *) &client_addr, &client_addr_len);if (client_sock == -1) {std::cout << "accept failed!" << std::endl;close(server_sock);return;}ssize_t read_len = 0;while ((read_len = read(client_sock, buff, sizeof(buff))) > 0) {ssize_t write_len_message = write(client_sock, buff, strlen(buff));if (write_len_message != strlen(buff)) {std::cout << "write failed!" << std::endl;close(server_sock);return;}memset(buff, 0, read_len);}close(client_sock);//可以省略,因为服务端关闭的时候客户端会自动关闭}close(server_sock);
}void run_client_02() {//创建一个子进程__pid_t pid = fork();if (pid == 0) {sleep(2);//子进程休眠2秒钟,//开启客户端int client = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in server_addr{};memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");server_addr.sin_family = AF_INET;server_addr.sin_port = htons(9527);int ret_conn = connect(client, (struct sockaddr *) &server_addr, sizeof(server_addr));while (0 == ret_conn) {printf("%s(%d):%s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__);char buff[256] = "";fputs("Input messages (Q to quit):", stdout);fgets(buff, sizeof(buff), stdin);if ((strcmp(buff, "q\n") == 0) || (strcmp(buff, "Q\n") == 0)) {break;}size_t len = strlen(buff);size_t send_len = 0;while (send_len < len) {ssize_t ret = write(client, buff + send_len, len - send_len);if (ret <= 0) {fputs("write failed:", stdout);close(client);std::cout << "client done" << std::endl;}send_len += ret;}memset(&buff, 0, sizeof(buff));size_t read_len = 0;while (read_len < len) {ssize_t ret = read(client, buff + read_len, len - read_len);if (ret <= 0) {fputs("read failed:", stdout);close(client);std::cout << "client done" << std::endl;}read_len += ret;}std::cout << "form server:" << buff << std::endl;}close(client);std::cout << "client done" << std::endl;} else if (pid > 0) {tcp_server_01();int status = 0;wait(&status);} else {std::cout << "fork failed!" << std::endl;}
}void tcp_02() {//创建一个子进程__pid_t pid = fork();if (pid == 0) {tcp_server_02();} else if (pid > 0) {for (int i = 0; i < 2; ++i) {run_client_02();}int status = 0;wait(&status);} else {std::cout << "fork failed!" << std::endl;}
}int main() {tcp_02();return 0;
}

效果图

TCP 套接字的 I/O 缓冲

我们知道,TCP 套接字的数据收发无边界。服务器端即使调用 1 次 write 函数传输 40 字节的数据,客户端也有可能通过 4 次 read 函数调用每次读取 10 字节。但此处也有一些疑问,服务器端一次性传输了 40 字节,而客户端居然可以缓慢地分批接收。客户端接收 10 字节后,剩下的 30 字节在何处等候呢?是不是像飞机为等待着陆而在空中盘旋一样,剩下 30 字节也 在网络中徘徊并等待接收呢?

实际上,**write 函数调用后并非立即传输数据,read 函数调用后也并非马上接收数据。**更准确地说,如下图所示,write 函数调用瞬间,数据将移至输出缓冲;read 函数调用瞬间, 从输人缓冲读取数据。

调用 write 函数时,数据将移到输出缓冲,在适当的时候(不管是分别传送还是一次性传送)传向对方的输入缓冲。这时对方将调用 read 函数从输入缓冲读取数据。这些 I/O 缓冲特性可整理如下。

  1. I/O 缓冲在每个 TCP 套接字中单独存在。
  2. I/O 缓冲在创建套接字时自动生成。
  3. 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
  4. 关闭套接字将丢失输入缓冲中的数据。

那么,下面这种情况会引发什么事情?理解了 I/O 缓冲后,其流程:

“客户端输入缓冲为 50 字节,而服务器端传输了 100 字节。”

这的确是个问题。输入缓冲只有 50 字节,却收到了 100 字节的数据。可以提出如下解决方案∶

填满输入缓冲前迅速调用 read 函数读取数据,这样会腾出一部分空间,问题就解决了。

其实根本不会发生这类问题,因为 TCP 会控制数据流。

TCP 中有滑动窗口(Sliding Window)协议,用对话方式呈现如下

套接字 A∶"你好,最多可以向我传递 50 字节。"

套接字 B∶"OK!"

套接字 A∶"我腾出了 20 字节的空间,最多可以收 70 字节。

套接字 B∶"OK!

数据收发也是如此,因此 TCP 中不会因为缓冲溢出而丢失数据。

TCP 的内部原理

TCP 通信三大步骤

  1. 三次握手建立连接
  2. 开始通信,交换数据
  3. 四次挥手断开连接

三次握手

定义套接字 A为客户端,定义套接字 B 为服务端

【第一次握手】套接字 A∶"你好,套接字 B。我这儿有数据要传给你,建立连接吧。"

【第二次握手】套接字 B∶"好的,我这边已就绪。"

【第三次握手】套接字 A∶"谢谢你受理我的请求。"

首先,请求连接的主机 A 向主机 B 传递如下信息∶

[SYN] SEQ:1000, ACK: -

该消息中 SEQ 为 1000,ACK 为空,而 SEQ 为 1000 的含义如下∶

"现传递的数据包序号为 1000,如果接收无误,请通知我向您传递 1001 号数据包。"这是首 次请求连接时使用的消息,又称 SYN。SYN 是 Synchronization 的简写,表示收发数据前传输 的同步消息。

接下来主机 B 向 A 传递如下消息∶

[SYN+ACK]SEQ:2000, ACK:1001

此时 SEQ 为 2000,ACK 为 1001,而 SEQ 为 2000 的含义如下∶ “现传递的数据包序号为 2000 如果接收无误,请通知我向您传递 2001 号数据包。” 而 ACK1001 的含义如下∶ "刚才传输的 SEQ 为 1000 的数据包接收无误,现在请传递 SEQ 为 1001 的数据包。"

对主机 A 首次传输的数据包的确认消息(ACK1001)和为主机 B 传输数据做准备的同步消息 (SEQ2000)拥绑发送,因此,此种类型的消息又称 SYN+ACK。

收发数据前向数据包分配序号,并向对方通报此序号,这都是为防止数据丢失所做的准备。

通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。因此,TCP 可以保证可靠的数据传输。最后观察主机 A 向主机 B 传输的消息∶

[ACK]SEQ:1001, ACK:2001

TCP 连接过程中发送数据包时需分配序号。

在之前的序号 1000 的基础上加 1,也就是分配 1001。此时该数据包传递如下消息∶

“已正确收到传输的 SEQ 为 2000 的数据包,现在可以传输 SEQ 为 2001 的数据包。”

这样就传输了添加 ACK 2001 的 ACK 消息。至此,主机 A 和主机 B 确认了彼此均就绪。

四次挥手

挥手过程:

套接字 A∶"我希望断开连接。"

套接字 B∶"哦,是吗?请稍候。"

套接字 B∶"我也准备就绪,可以断开连接。"

套接字 A∶"好的,谢谢合作。"

Linux C++ TCP编程相关推荐

  1. Linux下高性能网络编程中的几个TCP/IP选项

    Linux下高性能网络编程中的几个TCP/IP选项 转自:http://blog.chinaunix.net/u/12592/showart.php?id=2064847 最近在新的平台上测试程序,以 ...

  2. Linux下TCP网络编程-创建服务器与客户端

    一.前言 互联网概念诞生于20世纪60年代末,从9几年中国接入互联网开始到现在,生活的每个角落都能看到网络的使用.现在物联网时代.共享经济的到来,生活中不仅仅电脑.手机可以接入网络,身边的各个设备也能 ...

  3. Linux Socket网络编程UDP、TCP 阻塞与非阻塞 断线重连机制

    三种非阻塞模式的方法: (1) fcntl函数 int Mode = fcntl(sockfd, F_GETFL, 0);       //获取文件的Mode值     fcntl(sockfd, F ...

  4. 【Linux】网络编程三:TCP通信和UDP通信介绍及代码编写

    参考连接:https://www.nowcoder.com/study/live/504/2/16. [Linux]网络编程一:网络结构模式.MAC/IP/端口.网络模型.协议及网络通信过程简单介绍 ...

  5. Linux学习——网络编程基础及TCP服务器

    目录 一.网络采用分层的思想: 二.各层典型的协议: 三.网络的封包和拆包: 四.网络编程的预备知识 4.1.SOCKET 4.2 IP地址 4.3 端口号 4.4 字节序 五.TCP编程API TC ...

  6. Linux网络编程 之 TCP编程(七)

    目录 1. TCP客户端 - 核心函数 - 完整的TCP客户端程序 2. TCP服务端 - 核心函数 - 完整的TCP客户端程序 TCP编程的核心步骤和流程: 1. TCP客户端 核心函数: 创建一个 ...

  7. Linux 网络编程——TCP编程

    概述 TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的.可靠的.基于字节流的传输层通信协议. TCP 具有以下特点: 1)电话系统服务模式的抽象 2) ...

  8. 【Linux Socket 编程入门】05 - 拉个骡子溜溜:TCP编程模型代码分析

    (一) 看看以前学了啥 前面介绍了socket的分类,IP地址,端口号(port),常用的socket数据结构以及常用的函数.现在我们来看一个例子,看看socket编程究竟是什么. (二) 一图看懂客 ...

  9. 【Linux】网络篇二--TCP编程

    网络篇二--TCP编程 一.TCP编程实现 1.编程步骤 2.socket函数 3.bind函数 4.地址转换函数 5.listen函数 6.accept函数 7.connect函数 8.send函数 ...

最新文章

  1. Linux启动重启停止DNS,ubuntu怎么开机停止启动smbd
  2. Spring反转控制
  3. linux mysql 单机主从_MariaDB单机双实例主从复制
  4. 怎样让WinForms下DataGrid可以像ASP.NET下的DataGrid一样使用自定义的模板列
  5. Unrecognized option: -jrockit
  6. 深入理解 Objective-C:方法缓存
  7. SpringBoot+Shiro+ehcache实现登录失败超次数锁定帐号
  8. npm的常用配置项---npm工作笔记004
  9. AI又抢了人类职位,这回轮到银行销售人员了?
  10. java list打乱排序_JAVA Collections.shuffle打乱列表
  11. Python字符串isalnum()
  12. 高中信息技术—Python常见关键字及函数中英文对照
  13. BZOJ 1717: [Usaco2006 Dec]Milk Patterns 产奶的模式( 二分答案 + 后缀数组 )
  14. Levenberg-Marquardt(LM算法)
  15. 图片,PDF转换成文字
  16. 原型工具Axure:常用效果制作(选中、淘宝网导航、轮播图、toast效果、呼出键盘、省市二级联动、步进器、订单详情案例、中继器)
  17. 用word快速将数字字体换成新罗马详细简单方法
  18. 异形与铁血战士关系 时间线
  19. sklearn中KMeans重要参数n_clusters
  20. 【对比Java学Kotlin】协程-创建和取消

热门文章

  1. ASEMI三相整流桥和单相整流桥的详细对比
  2. 关于bash -c “bash命令“ 和 python -c “python命令“的用法
  3. 怎样测量地图上曲线的长度_用于测量地图曲线长度的米尺的制作方法
  4. 时频分析在工程中的应用
  5. 100个python算法超详细讲解:双色球
  6. OpenGL 图像绿幕抠图
  7. 关于 联想R9000P 中X-Rite Color Assistant 未能恢复为显示器自定义的ICC配置文件的解决办法
  8. 人工智能的几个研究方向
  9. vscode中对flake8(python静态代码检查工具)和yapf(python代码格式化工具)的设置
  10. F4和L4的一个区别 (CCM)