IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 网络编程:TCP的socket编程(Linux) -> 正文阅读

[网络协议]网络编程:TCP的socket编程(Linux)


一、简单介绍TCP

面向连接:TCP的客户端与服务端进行通信时,服务端需要先和客户端建立连接,确认双方都在线后,再进行发送数据
可靠:保证数据是可靠有序到达对端的
面向字节流:多次发送的数据在网络传输过程中是没有明显边界的(TCP粘包问题)。如客户端发送先发送123,再发送abc,TCP能保证接收到的数据是123abc有序的。但不能确定123和abc之间是分多少次发送的。

二、TCP编程流程

1.编程流程

TCP与UDP的区别在于,TCP是可靠传输的,在连接之前需要双方建立连接。
对于服务端而言,前期的准备工作是:1.创建套接字 2.绑定地址信息 3.监听
对于客户端而言,准备工作是:1.创建套接字 2.发起连接
在这里插入图片描述

2.TCP的发送/接收缓冲区

对于TCP发送/接收缓冲区的理解对了解整个TCP编程流程十分重要。
当一个新连接来到的时候,监听接口中的侦听套接字知道有一个连接请求(接收到一个新连接),但不是这个侦听套接字与客户端进行通信的,是侦听套接字调用accept函数创建一个新连接套接字,这个新连接套接字对客户端进行服务。每有一个新连接到来侦听套接字就会创建一个新连接套接字。侦听套接字只负责接收新连接。
然后收发数据流程就变成了客户端将数据放到客户端的发送缓冲区,经过网络协议栈层层封装到达对端(服务端),经过网络协议栈的层层分用达到服务端的接收缓冲区(新连接套接字的接收缓冲区),服务端再从接收缓冲区中拿到数据,从而实现通信。
在这里插入图片描述

三、编程接口

创建套接字和绑定地址信息的接口都和UDP协议中的接口相同,都是使用socket函数与bind函数,下面是TCP编程独有的接口

1.监听(listen)

在linten函数中,对参数backlog的理解十分重要,它表示内核当中已完成连接队列的大小。
操作系统内核当中有一个未完成连接队列和一个已完成连接队列。在调用listen函数进行监听等待连接时,当客户端发起连接,就将这个连接放入到未完成连接队列当中。客户端服务端一旦进行三次握手建立连接后,这个连接就会从未完成连接队列中放入到已完成连接队列中,服务端调用accept函数是从这个已完成连接队列中拿数据,将这个连接拿走并创建新连接套接字来服务。
已完成连接队列大小backlog决定了服务端的并发连接数(指的是同一时刻服务端能处理的最大连接数量上限,不是服务端能够接收的连接上限)
服务端能够接收的连接上限取决于操作系统对进程中打开文件描述符的上限。因为每有一个新的连接就是一个套接字描述符,套接字描述符本质也是文件描述符。
能和服务端建立连接的数量 = backlog + 1
在这里插入图片描述

2.客户端连接(connect)

在这里插入图片描述

3.服务端接收连接(accept)

accept函数是一个阻塞函数
在这里插入图片描述

4.发送数据(send)

在这里插入图片描述

5.接收数据(recv)

在这里插入图片描述

四、TCPsocket编程

对于UDP编程并没有引入多线程编程与多进程编程的概念,都是单线程编程,是因为UDP之间的收发数据较为简单,双方的收发数据都是由一个套接字来完成的。
TCP也存在单线程编程,但牵扯到accept创建的多个新连接套接字时,单线程编程就会出现一些问题:在编程中需要使用while循环进行循环接收连接,但使用while循环时,
1.当accept函数放到while循环中,tcp服务端只能接收一个客户端的新连接(不代表只能有一个客户端建立连接),并和这一个客户端进行多次收发数据。
2.当accept函数放到while循环外,可以接收多个客户端新连接,但每个连接只能收发一次。
为了解决单线程编程的问题,引入了多线程编程和多进程编程来解决问题。
客户端代码:

  1 #include <stdio.h>
  2 #include <unistd.h>                                                                                                            
  3 #include <string.h>
  4 #include <sys/socket.h>
  5 #include <netinet/in.h>
  6 #include <arpa/inet.h>
  7 int main()
  8 {
  9     int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 10     if(sockfd < 0)
 11     {
 12         perror("socket");
 13         return 0;
 14     }
 15     //地址信息结构
 16     struct sockaddr_in addr;
 17     addr.sin_family = AF_INET;
 18     addr.sin_port = htons(27015);
 19     addr.sin_addr.s_addr = inet_addr("1.14.151.67");
 20     //连接
 21     int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
 22     if(ret < 0)
 23     {
 24         perror("connect");
 25         return 0;
 26     }
 27
 28       while(1)
 29     {
 30         char buf[1024] = "我是客户端!";
 31         //发送
 32         send(sockfd, buf, strlen(buf), 0);
 33         memset(buf, '\0', sizeof(buf));//重置缓冲区
 34         //接收
 35         ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
 36         if(recv_size < 0)
 37         {
 38             perror("recv");
 39             continue;
 40         }
 41         else if(recv_size == 0)
 42         {
 43             printf("对端关闭连接\n");
 44             close(sockfd);
 45             return 0;
 46         }
 47         printf("%s\n", buf);
 48         sleep(1);
 49     }
 50     close(sockfd);
 51     return 0;
 52 } 

1.多线程编程

多线程编程流程:为了解决单线程的弊端,采用多线程的方式,让主线程去监听(只接收数据,不处理数据),然后主线程创建一堆工作线程,让工作线程去与客户端进行通信。
注意:多线程编程时需要给当前线程设置分离属性,让操作系统回收线程退出的资源,使用new在堆上开辟的空间需要delete释放
多线程服务端代码:

  1 #include <stdio.h>                                                                                                             
  2 #include <unistd.h>
  3 #include <string.h>
  4 #include <pthread.h>
  5 #include <sys/socket.h>
  6 #include <netinet/in.h>
  7 #include <arpa/inet.h>
  8 
  9 struct ThreadInfo
 10 {
 11     int newsockfd_;
 12 };
 13 
 14 void* TcpThreadStart(void* arg)//线程入口函数
 15 {
 16     pthread_detach(pthread_self());//给当前线程设置分离属性,让操作系统回收线程资源
 17     struct ThreadInfo* ti = (struct ThreadInfo*)arg;//对参数强转
 18     int newsockfd = ti->newsockfd_;
 19     while(1)
 20     {
 21         //接收数据
 22         char buf[1024] = {0};
 23         ssize_t recv_size = recv(newsockfd, buf, sizeof(buf) - 1, 0);
 24         if(recv_size < 0)
 25         {
 26 			perror("recv");
 27             continue;
 28         }
 29         else if(recv_size == 0)//等于0表示对端关闭连接
 30         {
 31             printf("对端关闭连接\n");
 32             close(newsockfd);
 33             break;
 34         }
 35         printf("%s\n", buf);
 36         memset(buf, '\0', sizeof(buf));//重置缓冲区
 37         strcpy(buf, "我是服务端!!!");
 38         //发送
 39         send(newsockfd, buf, strlen(buf), 0);
 40     }
 41     delete ti;
 42     return NULL;
 43 }
 44 
 45 int main()
 46 {                                                                                                                              
 47     int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//设置监听套接字
 48     if(listen_sock < 0)
 49     {
 50         perror("socket");
 51         return 0;
 52		}
 53     //设置地址信息结构
 54     struct sockaddr_in addr;
 55     addr.sin_family = AF_INET;
 56     addr.sin_port = htons(27015);
 57     //0.0.0.0 : 本地所有的网卡地址
 58     addr.sin_addr.s_addr = inet_addr("0.0.0.0");
 59 
 60     int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
 61     if(ret < 0)
 62     {
 63         perror("bind");
 64         return 0;
 65     }
 66     ret = listen(listen_sock, 1);
 67     if(ret < 0)
 68     {
 69         perror("listen");
 70         return 0;
 71     }                                                                                                                          
 72     while(1)
 73     {
 74         struct sockaddr_in cli_addr;
 75         socklen_t cli_addrlen = sizeof(cli_addr);
 76         //接收新连接
 77    		int newsockfd = accept(listen_sock, (struct sockaddr*)&cli_addr, &cli_addrlen);
 78         if(newsockfd < 0)
 79         {
 80             perror("accept");
 81             return 0;
 82         }
 83         printf("从客户端 %s:接收到新连接,端口:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
 84         struct ThreadInfo* ti = new ThreadInfo;
 85         ti->newsockfd_ = newsockfd;
 86         //创建线程
 87         pthread_t tid;
 88         ret = pthread_create(&tid, NULL, TcpThreadStart, (void*)ti);
 89         if(ret < 0)
 90         {
 91             close(newsockfd);
 92             delete ti;
 93             continue;
 94         }
 95     }
 96     close(listen_sock);//关闭套接字
 97     return 0;
 98 }

2.多进程编程

多进程编程流程:与多线程编程原理类似,让父进程进行监听(只接收数据,不处理数据),然后父进程创建子进程,让子进程与客户端进行通信。
注意:在创建子进程后,需要使用自定义信号处理方式修改父进程对于SIGCHLD信号的处理方式,不然子进程先退出之后就会产生僵尸进程,造成内存泄漏。
服务端代码:

  1 #include <stdio.h>                                                                                                             
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <string.h>
  5 #include <signal.h>
  6 #include <sys/wait.h>
  7 #include <sys/socket.h>
  8 #include <netinet/in.h>
  9 #include <arpa/inet.h>
 10 
 11 void sigcallback(int signo)//回调函数
 12 {
 13     printf("收到信号 : %d\n", signo);
 14     //当前的wait是进程等待的阻塞接口, 但是应用场景一定是子进程退出,
 15     //父进程收到了SIGCHLD信号之后, 才会回调sigcallback函数, 才会调用wait
 16     wait(NULL);
 17 }
 18 
 19 int main()
 20 {
 21     //自定义信号处理方式
 22     signal(SIGCHLD, sigcallback);
 23     //创建监听套接字
 24     int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 25     if(listen_sock < 0)
 26     {
 27			perror("socket");
 28         return 0;
 29     }
 30     //地址信息结构
 31     struct sockaddr_in addr;                                                                                                   
 32     addr.sin_family = AF_INET;
 33     addr.sin_port = htons(27015);
 34     addr.sin_addr.s_addr = inet_addr("0.0.0.0");
 35     int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
 36     if(ret < 0)
 37     {
 38         perror("bind");
 39         return 0;
 40     }
 41 
 42     ret = listen(listen_sock, 5);
 43     if(ret < 0)
 44     {
 45         perror("listen");
 46         return 0;
 47     }
 48
 49
 50     while(1)
 51     {
 52         int new_sock = accept(listen_sock, NULL, NULL);
 53         if(new_sock < 0)
 54         {
 55             continue;
 56         }
 57 
 58         //创建子进程
 59         int pid = fork();
 60         if(pid < 0)
 61         {
 62             //创建子进程失败, 但接受新连接成功,需要关闭新连接套接字
 63             close(new_sock);
 64             continue;
 65         }
 66         else if(pid == 0)
 67         {
 68             //子进程                                                                                                           
 69             close(listen_sock);
 70             while(1)
 71             {
 72                 //recv   and      send
 73                 char buf[1024] = {0};
 74
 75                 ssize_t recv_size = recv(new_sock, buf, sizeof(buf) - 1, 0);
 76                 if(recv_size < 0)
 77                 {
 78                     perror("recv");
 79                     continue;
 80                 }
 81                 else if(recv_size == 0)
 82                 {
 83                     printf("对端关闭连接\n");
 84                     close(new_sock);
 85                     //子进程退出
 86                     exit(1);
 87                 }
 88                 printf("client say: \"%s\"\n", buf);
 89                 memset(buf, '\0', sizeof(buf));
 90                 strcpy(buf, "我是服务端!");
 91                 send(new_sock, buf, strlen(buf), 0);                                                                           
 92             }
 93         }
 94         else
 95         {
 96             //父进程
 97				close(new_sock);
 98         }
 99     }
100     return 0;
101 } 

总结

TCP的socket编程相对于UDPsocket编程更为复杂,但各有优劣。学习TCP及UDP的网络编程有助于理解传输层协议TCP和UDP。

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-29 09:42:12  更:2021-08-29 09:42:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 21:15:52-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码