【计算机网络】TCP实战

其实有了UDP的基础,TCP不管怎么说学习起来都还是比较舒服的,至少是比直接就学习TCP的感觉好。

这篇文章最多就是介绍一下起手式,如果想带业务的话和UDP那篇是完全一样的,就不进行演示了。

总的来说还是很简单的。

目录

Echo

我们还是从最简单的不带业务的Echo开始。

服务端起手式

服务器起手式

首先要说明的一点是,TCP是面向字节流,有连接。

而UDP创建好套接字后不管连接,直接recvfrom,sendto就可以发送。

在TCP编码中会体现出有连接的特点,面向字节流会在理论中提及,代码中实践。


创建套接字

首先与UDP不同的是,UDP的第二个参数是SOCK_DGRAM

c 复制代码
_listenfd = ::socket(AF_INET, SOCK_STREAM, 0);

关于这两个参数不同含义更详细的放在下图中了。

另外,这次使用listenfd接收,以前我们获得的就是sockfd,直接使用这个描述符进行收发,那么叫listenfd肯定是有特别用意。


进行bind

bind没什么好说的,仍旧是要注意填参问题。

cpp 复制代码
int n = ::bind(_listenfd, (struct sockaddr *)&local, len);

进入listen状态

从这里就可以看到面向连接的痕迹了。

那么什么叫listen状态?

我们举一个小例子:

UDP就是无人售货店,不许用老板看店即可买东西。

但是TCP就是有人售货店,需要老板在才可以买东西,而TCP就是那个有人售货店,listen就是设置为老板模式,这样就可以接客了。

第一个参数就是你得到的listenfd。

第二个参数是连接队列长度,这个值我们暂时先不用管,一般默认设置为4, 8, 16即可,不需要太大。

c 复制代码
n = ::listen(_listenfd, gbacklog);

服务端LOOP

accept

来了新的连接我们就要去accept了

换句话说就是在有人售货店中现在如果来客人了老板就需要去接客。

cpp 复制代码
int sockfd = ::accept(_listenfd, (sockaddr *)&peer, &len);

参数是输出型参数,来一个连接就可以获得这个连接的基本信息。

但是要注意填参时一定要正确填参,虽然addrlen是一个输出型参数,但仍然需要正确初始化,不能不初始化。

我就犯了这个错误,导致有时错误(accept或者connect错误,connect在客户端会提到),甚至搞的我以为这是tcp特性...

但是我们注意到返回值是一个文件描述符!?那么他是什么?与listenfd有何区别?我们举一个例子进行理解:

上图可以形象的帮助理解返回值。

其中饭店 = 服务器

客户 = 新连接

揽客员 = listenfd(socket返回值)

服务员 = sockfd (accept返回值)

listenfd不直接提供服务,sockfd才直接提供服务!

所以在TCP服务端我们一般将socket返回值叫做监听套接字。

没有连接时就会陷入阻塞状态,accept失败返回-1(这里可以理解为揽客员都把路人拉到店里面,但是路人又临时有事离开了,导致拉客失败~)。


下段代码是TCP的框架代码,与UDP有很大的不同。

cpp 复制代码
void Loop()
{
    _isrunning = true;
    while (_isrunning)
    {
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = ::accept(_listenfd, (sockaddr *)&peer, &len);
        if (sockfd < 0)
        {
            continue;
        }
        // version 0 :提供长服务。
        service(sockfd);
    }
    _isrunning = false;
}

与UDP有很大的不同:

每一次循环都对应着一个新连接,我们随后要对这个新连接进行长服务;

而UDP是不管来的是谁,都统一处理。


读写操作

我们在长服务中进行读写操作。

因为我们是面向字节流,而管道、文件也都是流。

且Linux下一切皆文件,管道、文件都是文件,所以都是可以用文件操作read,write进行读写,所以这里也可以使用文件操作!

cpp 复制代码
void service(int sockfd)
{
    // 长服务
    while (true)
    {
        char inbuffer[1024];
        int n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            int m = write(sockfd, inbuffer, n);
            if (m < 0)
            {
                break;
            }
        }
        else if (n == 0)
        {
            break;
        }
        else
        {
            break;
        }
    }
    ::close(sockfd);
}

但是这里有一个细节要注意:read的返回值>0是表示读到的字节数(无\0),==0时表示读到文件结尾,在这里表示客户端结束。

就想吃在管道那里,写端管了,读端读出的自然都是0了。

客户端起手

socket

cpp 复制代码
int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);

注意客户端仍旧是用socket的返回值进行通信。


connect

同样,客户端还是不用bind,在UDP那是sendto时OS进行绑定,而在这就是connect时帮我们自动绑定。

cpp 复制代码
sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);
peer.sin_port = htons(port);
peer.sin_family = AF_INET;
int n = ::connect(sockfd, (sockaddr *)&peer, sizeof(peer));

客户端LOOP

这里没啥好说的,就是普通的write + read。

但是要注意:由于是面向字节流,所以这里的处理是有问题的!具体如何操作请看用户自定义协议与序列化

cpp 复制代码
while (true)
{
    std::cout << "Please enter#";
    std::string line;
    getline(std::cin, line);
    n = write(sockfd, line.c_str(), line.size());
    if (n < 0)
    {
        break;
    }
    char inbuffer[1024];
    n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
    if (n > 0)
    {
        inbuffer[n] = 0;
        std::cout << inbuffer << std::endl; 
    }
}

完整代码链接

验证

但是当我们多开一个客户端就会发现:一个服务端只能服务一个客户端。

原因在于我们当前是串行,必须等当前客户端退出才能accept下一个客户端,并没有处理并发客户端的能力。

所以改进就来了

改进

方案一

多进程方案。

我们知道子进程是会继承父进程的代码和数据的,同样的文件描述符表也会继承,因此我们就可以利用这个特点让子进程去执行代码和数据。

首先我们fork之后就要将不需要的文件描述符关闭

对于子进程来说,是怕误操作;

对于父进程来说,是为了防止文件描述符泄露,因为进程具有独立性,当子进程继承了后,就与父进程相互不影响了。若是父进程不关闭,一直申请fd却步关闭那么就造成了资源泄露。

但是注意,由于父进程要进行等待,所以此时仍然是串行。

想要解决有两种方法

  1. 由于子进程结束时会发送SIGCHLD信号,于是进行信号忽略signal(SIGCHLD, SIG_IGN)另外这也是最佳方案
  2. 下段代码所示:我们进行再次fork,并让子进程退出,所以此时孙子进程成为了孤儿进程,孤儿进程的父进程是OS,结束归OS管,完美的利用了OS的特性。
cpp 复制代码
pid_t pid = fork();
if (pid == 0)
{
    if (fork() > 0) exit(0);
    // 子进程关闭不需要的fd,防止误操作。
    ::close(_listenfd);
    service(sockfd, inetaddr);
    exit(0);
}
waitpid(pid, nullptr, 0);
// 父进程关闭不需要的fd,防止内存泄漏。
::close(sockfd);

方案二

多线程

我们创建一个新线程,让新线程去执行service即可。

验证

此时同时启动两个客户端也可以完美的进行并发了。

相关推荐
Aileen_0v019 分钟前
【AI驱动的数据结构:包装类的艺术与科学】
linux·数据结构·人工智能·笔记·网络协议·tcp/ip·whisper
花鱼白羊1 小时前
TCP Vegas拥塞控制算法——baseRtt 和 minRtt的区别
服务器·网络协议·tcp/ip
CHENGlady4 小时前
传输层协议分析头歌
计算机网络
Jacen.L7 小时前
使用TC命令模拟弱网丢包
计算机网络
总是学不会.8 小时前
第五篇:前后端如何“扯皮”——HTTP 在开发中的应用
java·网络·网络协议·http·开发
sone121389 小时前
计算机网络(第8版)第四章 网络层(4.7.1~4.7.3)
服务器·网络·计算机网络
m0_7482382710 小时前
WebClient HTTP 请求问题处理模板(泛型响应、忽略 SSL 证书等)
网络协议·http·ssl
୧⍢⃝୨ LonelyCoder10 小时前
FreePBX修改IP地址和端口以及添加SSL证书开启HTTPS访问
tcp/ip·https·ssl
轩辰~11 小时前
网络协议入门
linux·服务器·开发语言·网络·arm开发·c++·网络协议
Ven%12 小时前
如何在防火墙上指定ip访问服务器上任何端口呢
linux·服务器·网络·深度学习·tcp/ip