【计算机网络】TCP实战

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

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

Echo

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

服务端起手式

服务器起手式
首先要说明的一点是,TCP是面向字节流,有连接。
而UDP创建好套接字后不管连接,直接recvfrom,sendto就可以发送。

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


创建套接字
首先与UDP不同的是,UDP的第二个参数是SOCK_DGRAM

_listenfd = ::socket(AF_INET, SOCK_STREAM, 0);

关于这两个参数不同含义更详细的放在下图中了。
在这里插入图片描述
另外,这次使用listenfd接收,以前我们获得的就是sockfd,直接使用这个描述符进行收发,那么叫listenfd肯定是有特别用意。


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

int n = ::bind(_listenfd, (struct sockaddr *)&local, len);

进入listen状态

在这里插入图片描述

从这里就可以看到面向连接的痕迹了。
那么什么叫listen状态?
我们举一个小例子:
UDP就是无人售货店,不许用老板看店即可买东西。
但是TCP就是有人售货店,需要老板在才可以买东西,而TCP就是那个有人售货店,listen就是设置为老板模式,这样就可以接客了。

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

n = ::listen(_listenfd, gbacklog);

服务端LOOP

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

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有很大的不同。

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进行读写,所以这里也可以使用文件操作!

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

int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);

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


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

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

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的特性。
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即可。

验证

此时同时启动两个客户端也可以完美的进行并发了。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值