一.TCP协议概念

与UDP协议相同,TCP协议也是应用在传输层的协议。虽然都是应用在传输层,但是使用方式和应用场景上大不一样。TCP协议具有:有连接(可靠)、面向字节流的特点。

(一).有连接

所谓有连接,指的是信息传递是建立在通信双方已经提前“取得了联系”,可以理解为提前创建了一个用于通信的管道。并且TCP的通信具有指向性,发送的信息只能“点对点”式的发送。而UDP协议下发送信息不需要连接,具体可以参考这篇文章:UDP协议与相关套接字编程

因为TCP的通信建立在双方取得联系的基础上,因此不存在数据丢失的问题。如果接收方没有收到信息,那么发送方会继续发送数据直到接收方获取到为止。这也就是为什么TCP协议下的网络通信具有可靠性。

(二).面向字节流

和系统输入输出流一样,TCP协议也存在一个缓冲区。当传输数据时,如果数据太短那么会一直保存在缓冲区中,直到数据长度达到发送要求后统一发送给接收方。如果数据太长,那么会进行拆分,一部分一部分的进入缓冲区然后发送。这里需要注意的是因为缓冲区的存在,一份数据可能会拆开来发送,这样接收方一次收到的数据可能就是不完整的,因此需要自定义通信协议(检测数据是否已经完整)确保目标数据完整后再使用,具体方式在套接字编程代码中会说明。而UDP协议基于面向数据报的特点,不存在缓冲区的概念,因此接收数据的完整性得以保障。

二.TCP套接字编程

(一).系统调用接口

①获取套接字/创建套接字结构体/绑定

在UDP套接字编程的博客中已经介绍了相关接口和使用方式,不再赘述:UDP协议与相关套接字编程

TCP的使用方式与UDP基本一致,参数略有不同。

获取套接字:

socket函数第二个参数为套接字类型,TCP为面向字节流,即SOCK_STREAM,需要注意的是,TCP协议有两个套接字文件描述符,其中socket接口返回的叫做监听套接字,用于获取连接;还有一个服务套接字用于和对端网络通信。

int listensocket = -1;
listensocket = socket(AF_INET, SOCK_STREAM, 0);

创建结构体:

与UDP创建结构体方式一致。

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = INADDR_ANY;

绑定:

与UDP绑定方式一致

int i = bind(_listensocket, (struct sockaddr *)&addr, sizeof(addr));
if (i < 0) ...

②设置监听状态

因为TCP协议具有连接性的特定,因此需要调用listen接口提前设置一个监听状态。

listen接口第一个参数为监听套接字。

第二个参数为最大连接数量,与TCP建立连接的对端都会记录在一个队列中,这个参数定义的就是该队列最大长度(队列长度为参数值+1)。当队列长度达到最大时,再有对端想要建立连接就会返回一个错误。参考如下:

设置监听成功会返回0,失败返回-1。

int i = listen(listensocket, 20);
if(i < 0) ...

③服务端等待连接

系统接口accept用于与对端获取连接。如果没有主机与本端连接,那么会阻塞在accept函数上。

参数方面,第一个是监听套接字。

第二个和第三个都是输出型参数,和recvfrom接口一样,用于获取对端中记录IP地址和端口号的结构体struct sockaddr。

当对端尝试与本机建立TCP连接后,如果accept成功,那么会返回服务套接字,之后的网络通信行为都使用服务套接字完成。如果accept失败,那么会返回-1,示意如下:

struct sockaddr_in addr;//用于获取对端IP地址&端口号
bzero(&addr, sizeof addr);
socklen_t addr_len;
int servesocket = accept(listensocket, (struct sockaddr*)&addr, &addr_len);
if(servesocket < 0) ...

④客户端连接

服务端使用accept阻塞后,客户端需要调用connect接口主动与服务端建立连接。

需要注意的是,与UDP协议一样,客户端在建立连接前不需要手动绑定自己的端口号和IP地址,采用系统自动绑定的方式让服务端获取。

接口参数方面,第一个参数为本端(客户端)套接字文件描述符(socket函数返回值)。

第二个参数是struct sockaddr类型,记录的是服务端IP地址和端口号,用于确定连接哪个服务器。

第三个参数记录的是第二个参数对象的长度。

连接成功返回0, 失败返回-1。

int sock = socket(...);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);//记录服务端的端口号
addr.sin_addr.s_addr = inet_addr(ip);//记录服务端的IP地址
int i = connect(sock, (struct sockaddr*)&addr, sizeof addr);
if(i < 0) ...

⑤发送数据

调用write/send系统接口发送数据。

因为服务套接字本质就是一个文件描述符,因此可以直接使用write接口发送数据,具体方式与普通文件别无二致。

send接口和write接口使用上差别不大,前三个参数含义相同,最后一个参数代表发送策略,一般填0。

但有一点需要强调,write和send的第一个参数在服务端是服务套接字,因为每一个客户端(对端)的服务套接字各不相同,使用服务套接字可以标定唯一的客户端。在客户端就采用socket接口返回的套接字文件描述符即可。

char buf[1024];
...//生产网络数据
//**********服务端
int i = send(servesocket, buf, strlen(buf), 0);
//or:   write(servesocket, buf, strlen(buf));//**********客户端
int i = send(sock, buf, strlen(buf), 0);
//or:   write(sock, buf, strlen(buf));
if(i < 0) ...

⑥获取数据

调用recv/read接口获取数据。

道理同发送数据,不再赘述,看完示例就明白了:

char buf[1024];
memset(buf, 0, sizeof buf);
//***********服务端
int i = recv(servesocket, buf, sizeof buf, 0);
//or:   read(servesocket, buf, sizeof buf);
//***********客户端
int i = recv(sock, buf, sizeof buf, 0);
//or:   read(sock, buf, sizeof buf);
if(i <= 0) ...

但这里有一个疑问:

首先我们知道UDP协议通信中,接收数据时规定接收长度最大为buf大小-1。这是防止接收数据超过buf大小,导致不能设置最后一位为'\0':

int i = recvfrom(sockfd, buf, sizeof buf - 1, ...);
buf[i] = 0;

那为什么采用TCP协议接收数据时不再规定接收长度为size - 1呢?

这是因为TCP协议具有面向字节流的特点,也就是第一部分介绍面向字节流时说的一次传输数据可能不完整,需要多次传输。举个例子,假如客户端发送了"abcdefg"这个字符串,但是TCP的缓冲区太小,只存放了efg,那么服务端第一次接受的就是abcd,第二次接收时才能收到efg。因此接收长度应该是buf大小。

那么怎么判断何时数据全部接收了呢?需要自定义通信协议,在下一节会说明。

(二).模拟TCP服务端与客户端

①定制协议

基于上文,我们知道TCP协议的通信中一份数据可能是经过多次分批发送的。因此需要根据数据内容判断哪次接收后一份数据传输完毕。

怎么判断呢?我们可以这样:在一份数据的开头添加"数据长度"\r\n",结尾添加"\r\n"。

例:5\r\nabcde\r\n

接收数据时,首先收到数据长度和\r\n,知道有一份新的数据过来了且知道了数据长度。当再次收到\r\n,一份数据就完整传输完毕。当然这样的方式也可以理解为给数据添加了报头和报尾。

#define SEP "\r\n"
#define SEP_LEN strlen(SEP)
string Decode(string *str) // 解除自定义协议(获取数据)
{int pos = str->find(SEP);if (pos == string::npos)return "";int len = atoi(str->substr(0, pos).c_str());int size = str->size() - pos - SEP_LEN * 2;if (size >= len){str->erase(0, pos + SEP_LEN);string ret = str->substr(0, len);str->erase(0, len + SEP_LEN);return ret;}elsereturn "";
}
bool Encode(string *buf) // 添加自定义协议 length\r\nxxxxxxx\r\n
{string ret = std::to_string(buf->size());ret += SEP;ret += *buf;ret += SEP;*buf = ret;return true;
}

②序列化与反序列化

当然上文有一个疑点,如果数据内容不是字符串呢,比如结构体,那么协议该怎么定制呢。对于这个问题答案很简单,通常情况下在网络通信中,把要传入网络的数据全部转成字符串的形式即可。通俗来讲,比如结构体就把每一个数据成员转成一个字符串,这个过程就叫做序列化。将网络中的字符串型数据转成主机中目标类型(比如字符串转成结构体)的过程就叫做反序列化。

图示如下:

③服务端

服务端的任务主要有三个:建立TCP通信,接收数据,发送数据。

当然,TCP的服务端处除了需要建立监听外,在连接一个客户端后,需要创建一个子进程负责和这个客户端通信,父进程则继续循环等待新的客户端来连接。除了上述方式完成多客户端连接,也可以使用多线程的方式。如果是采用多进程版的还需要注意一点,子进程需要关闭监听套接字因为子进程不用于获取连接;父进程要关闭服务套接字,因为父进程只用于建立与客户端的连接,如果不关闭的话随着连接客户端数量的增加会导致出现文件描述符不够的问题。

当接收到数据后,先将网络中的数据去掉自定义协议即去掉报头,之后将字符串类型的数据反序列化成主机对象类型。当处理完数据向客户端回传时,先将数据序列化成字符串类型便于网络传输,之后添加自定义协议(报头)。当然通信时采用accept返回的服务套接字。

伪代码如下:

class Server
{static void serveFunc(int sock, string ip, uint16_t port) // 用于子进程和客户端通信{...//接收数据(去除协议+反序列化)、处理数据、发送数据(添加协议+序列化)}public:...void initServer(){...//获取监听套接字、创建结构体、绑定listen(_listensocket, 20);//监听}void startServer(){signal(SIGCHLD, SIG_IGN);//分离父子进程,使子进程可以自动回收而不是变成僵尸进程while (true)//父进程循环,等待多个客户端连接{...//创建接收客户端的结构体int servesocket = accept(_listensocket, (struct sockaddr *)&addr, &len);//连接客户端,获取服务套接字if (fork() == 0)//子进程负责和特定客户端通信{close(_listensocket);//子进程通信不需要监听套接字serveFunc(servesocket, ip, port);//通信函数exit(0);//子进程退出}close(servesocket);//父进程不需要服务套接字}}private:string _IP;uint16_t _port;int _listensocket;
};

④客户端

客户端有三个任务:主动与服务端建立连接、发送数据、获取数据。

与服务端建立连接就采用connect接口,与UDP通信的客户端一样不需要自己绑定IP和端口号,而让操作系统自动绑定(避免一个主机上多个服务端同时绑定同一个IP地址和端口号)。

发送数据和获取数据的思路与服务端一致,发送数据前先序列化后添加协议,获取数据后先去除协议再反序列化。通信时用socket函数返回值即可。

伪代码如下:

int main(int argc, char* argv[]){...//获取套接字、根据服务端IP和端口号创建结构体(用于connect)//无需bind    //建立与服务端连接int i = connect(sock, (struct sockaddr*)&addr, sizeof addr);...//与服务端通信,发送数据、接收数据close(sock);return 0;}

如有错误,敬请斧正

Linux——TCP协议与相关套接字编程相关推荐

  1. 基于UDP协议的socket套接字编程 基于socketserver实现并发的socket编程

    基于UDP协议 的socket套接字编程 1.UDP套接字简单示例 1.1服务端 import socketserver = socket.socket(socket.AF_INET,socket.S ...

  2. TCP与UDP协议,socket套接字编程,通信相关操作

    文章目录 TCP与UDP协议 TCP协议 ==三次握手== ==四次挥手== UDP协议 TCP与UDP的区别 应用层 socket套接字 代码优化 循环通信 半连接池 粘包问题 TCP与UDP协议 ...

  3. TCP与UDP协议、socket套接字编程、通信相关操作(cs架构软件)、TCP黏包问题及解决思路

    OSI七层协议 传输层 1.PORT协议:前面讲过 2.TCP协议与UDP协议:规定了数据传输所遵循的规则(数据传输能够遵循的协议有很多,TCP和UDP是较为常见的两个) TCP协议 基于TCP传输数 ...

  4. 《Unix网络编程》卷一(简介TCP/IP、基础套接字编程)

    通常说函数返回某个错误值,实际上是函数返回值为-1,而全局变量errno被置为指定的常值(即称函数返回这个错误值). exit终止进程,Unix在一个进程终止时总是关闭该进程所有打开的描述符. TCP ...

  5. 【Linux】网络套接字编程

    前言 在掌握一定的网络基础,我们便可以先从代码入手,利用UDP协议/TCP协议进行编写套接字程序,明白网络中服务器端与客户端之间如何进行连接并且通信的. 目录 一.了解源目的IP.端口.网络字节序.套 ...

  6. 计算机网络套接字编程实验-TCP多进程并发服务器程序与单进程客户端程序(简单回声)

    1.实验系列 ·Linux NAP-Linux网络应用编程系列 2.实验目的 ·理解多进程(Multiprocess)相关基本概念,理解父子进程之间的关系与差异,熟练掌握基于fork()的多进程编程模 ...

  7. Linux之socket套接字编程20160704

    介绍套接字之前,我们先看一下传输层的协议TCP与UDP: TCP协议与UDP协议的区别 首先咱们弄清楚,TCP协议和UCP协议与TCP/IP协议的联系,很多人犯糊涂了,一直都是说TCP/IP协议与UD ...

  8. 专题 15 TCP套接字编程

    概述 存在三种套接字:流式套接字(SOCK_STREAM).数据报套接字(SOCK_DGRAM)和原始套接字(SOCK_RAW). TCP套接字工作流程: 首先,服务器端启动进程,调用Socket创建 ...

  9. 套接字编程--1(UDP协议编程,端口号,传输层协议,网络字节序)

    传输层的协议: ip地址: 在网络中唯一标识一台主机 IPV4:uint32_t DHCP NAT IPV6 : uint8_t addr[16] -向前并不兼容IPV4 每一条数据都必须包含源地址和 ...

最新文章

  1. 面试前临时抱佛脚——常见的Shell脚本面试题
  2. leetcode算法题--数组中数字出现的次数
  3. C++程序设计之保存和读取二进制文件
  4. linux 下 select 函数的用法
  5. 新生代农民工必看:模拟器eNSP安装教程(附下载链接)
  6. 笔记-高项案例题-2009年上-需求管理
  7. jmeter语言设置
  8. 我是如何自学 Python 的,分享一下经验
  9. 95-136-043-源码-Operator-LegacyKeyedCoProcessOperator
  10. byte用json存 c++_玩转golang——JSON高性能自动字段名
  11. java导出word的几种方式
  12. 一款超级炫酷的编辑代码的插件 Power Mode
  13. 公司官网建站笔记(四):从阿里云将域名转出,并将域名转入腾讯云
  14. onenote网页版如何打开链接弄到客户端
  15. Ubuntu虚拟机全屏问题
  16. 一人一猫旅行记之浅析序列化及原理
  17. 大学生面试20个经典问题及回答思路
  18. aps是什么意思_aps是什么意思
  19. Delphi7微信、支付宝扫码支付源码
  20. win10无线信号强度测试软件,如何在Win10 1909上确定Wi-Fi信号强度

热门文章

  1. DFIG控制10-b: 双馈发电机的转矩方程推导
  2. 搭载鸿蒙系统,全新华为移动无线路由器Pro发布,强,不止是一点点
  3. nginx跨域漏洞问题处理
  4. 刚体转动的定律及定理
  5. 被指同组抄袭!南大计算机这篇GAN论文引争议
  6. 一起掌握常用IC:ADS1112 ADC芯片
  7. 多测师_Python 介绍
  8. docker服务重启后自动重启容器
  9. Web---Cookie技术(显示用户上次登录的时间、显示用户最近浏览的若干个图片(按比例缩放))
  10. STM32单片机(六). 传感器的使用