Linux 系统编程 学习:008-基于socket的网络编程3:基于 TCP 的通信
阅读原文时间:2023年07月10日阅读:2

上一讲我们介绍了 基于UDP 的通信

这一讲我们来看 TCP 通信。

TCP(Transmission Control Protoco 传输控制协议)。

TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式:

  • 基于流的方式;

  • 面向连接;

  • 可靠通信方式;

  • 在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销;

  • 通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。

为满足TCP协议的这些特点,TCP协议做了如下的规定:

  • 数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;
  • 到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认;
  • 超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片;
  • 滑动窗口:TCP连接每一方的接收缓冲空间大小都固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据,TCP在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出;
  • 失序处理:作为IP数据报来传输的TCP分片到达时可能会失序,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层;
  • 重复处理:作为IP数据报来传输的TCP分片会发生重复,TCP的接收端必须丢弃重复的数据;
  • 数据校验:TCP将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验和有差错,TCP将丢弃这个分片,并不确认收到此报文段导致对端超时并重发。

%% 时序图
sequenceDiagram
participant Server
participant Client

%% Note right of Client: Client主动连接->
Note right of Server: 双方都创建socket对象

Server ->> Server: socket
Client ->> Client: socket

Note left of Server: 服务器一般绑定端口号
Server ->> Server: bind

Note left of Server: 服务器监听是否有连接请求
Server ->> Server: listen

Note left of Client: 客户端请求链接
Client ->> Server: connect

Server ->> Server: accept

Note right of Server: 收发消息
Client -->> Server: send/recv
Server -->> Client: send/recv

Note right of Server: 关闭连接

Client --> Client: close
Server --> Server: close

根据流程图,我们知道,在UDP通信中,使用到了这些函数:socket()bind()sendto()recvfrom()

上面的函数我们在《基于UDP 的通信》 中已经讲过,这里不再重复了。

在TCP中,多了这几个函数:listen()connect()accept()

服务器调用listen 监听 客户端的 connectlisten成功时,服务器使用由accept获取到的新的套接字进行通信。

当客户端调用connect函数时,将引发三次握手过程:客户端首先发送SYN请求分组,此时服务端会将请求放入SYN队列,同时向客户端发送ACK确认报文,然后客户端向服务端再次发送ACK报文。服务端收到ACK确认报文后,将SYN里的连接请求移入ACCEPT队列。此时三次握手结束,即TCP连接成功建立。然后内核通知用户空间的阻塞的服务进程,服务进程调用accept仅仅是从ACCEPT队列里取出一个连接而已。也就是说客户端调用connect连接服务器,与服务器调用accept“接受”连接是两个独立的过程。

参考:《服务端不调用accept,客户端connect能否成功?》

listen

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

描述: 将尚未建立连接的socket转换为被动socket,并监听发给这个被动socket的connect请求。

参数解析:

sockfd:由socket函数成功返回的值

backlog :内核应该为相应套接口排队的最大连接个数(不是用来限制socket的最大连接数),一般为以下两个队列的大小之和,即未完成三次握手队列 + 已经完成三次握手队列。即:TCP模块允许的已完成三次握手过程(TCP模块完成)但还没来得及被应用程序accept的最大链接数。

内核为任何一个给定的监听套接口维护两个队列:

1、未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接口处于SYN_RCVD状态。

2、已完成连接队列(completed connection queue),每个已完成TCP三次握手过程的客户对应其中一项。这些套接口处于ESTABLISHED状态。

当来自客户的SYN到达时,TCP在 未完成连接队列 中创建一个新项,然后响应以三次握手的第二个分节:服务器的SYN响应,其中稍带对客户SYN的ACK(即SYN+ACK)。这一项一直保留在未完成连接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。

返回值: 成功返回0,失败返回-1,置errno:

  • EADDRINUSE:另一个套接字已在同一端口上侦听。
  • EADDRINUSE:(Internet域套接字)sockfd引用的套接字以前没有绑定到地址,在尝试将其绑定到临时端口时,确定临时端口范围中的所有端口号当前都在使用中。
  • EBADF:参数sockfd不是有效的描述符。
  • ENOTSOCK:文件描述符sockfd没有引用套接字。
  • EOPNOTSUPP:套接字的类型不支持listen()操作。

主动socket和被动socket

一般来说,使用socket函数创建的socket默认是主动socket,这意味着一个主动的socket可以调用connect跟一个被动socket建立一个连接,对主动socket来说,这叫主动打开。

被动socket是一个通过调用listen函数监听要发起连接的socket,当被动socket接受一个连接通常称为被动打开。

在大多数网络程序中,服务端会作为被动socket被动接受连接,而客户端会作为主动socket主动发起连接。

服务端通过socket函数创建的socket是主动socket,而listen函数就是把这个还未接受连接的主动socket转换为被动socket,因为服务端只需要被动接受客户端的连接请求。

Linux系统设置未连接队列最大数限制

linux系统tcp/ip协议栈有个选项可以设置未连接队列大小限制tcp_max_syn_backlog

可以通过命令:cat /proc/sys/net/ipv4/tcp_max_syn_backlog 查看

Linux 系统中提供somaxconn这个参数,它定义了系统中每一个端口最大的监听队列的长度,这是个全局的参数,默认值为128

可以通过命令: cat /proc/sys/net/core/somaxconn 查看

connect

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);

描述: 连接一个被动socket

参数解析:

sockfd:主动socket

addr:目的地址

addrlen:地址属性的长度(addr的大小)

返回值:成功返回0,失败返回-1,置errno:

  • EAFNOSUPPORT:传递的地址在其sau family字段中没有正确的地址系列。

  • EAGAIN :路由缓存中的条目不足。

  • EALREADY:套接字未阻塞,上一次连接尝试尚未完成。

    EBADF:文件描述符不是描述符表中的有效索引。

  • ECONNREFUSED:没有人监听远程地址。

  • EFAULT :套接字结构地址在用户的地址空间之外。

  • EINPROGRESS:套接字未阻塞,无法立即完成连接。可以通过选择要写入的套接字来选择(2)或轮询(2)以完成。

  • EINTR :系统调用被捕获的信号中断。

  • EISCONN:套接字已连接。

  • ENETUNREACH:无法访问网络。

  • ENOTSOCK:sockfd不是套接字。

  • EPROTOTYPE:套接字类型不支持请求的通信协议。例如,在尝试将UNIX域数据报套接字连接到流套接字时,可能会发生此错误。

  • ETIMEDOUT:尝试连接时超时。服务器可能太忙,无法接受新连接。请注意,对于IP套接字,当服务器上启用Syncookie时,超时可能非常长。

accept

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <sys/socket.h>

int accept4(int sockfd, struct sockaddr *addr,
            socklen_t *addrlen, int flags);

描述: 从内核的ACCEPT队列中取出对应被动socket的连接,关于连接的有关属性填入addr中。

参数解析:

sockfd:对应的被动socket

addr:保存连接方的addr属性的容器

len:addr属性的长度

返回值: 成功返回可用于连接的新socket,失败返回-1,置errno:

此外,可能会返回新套接字的网络错误以及为协议定义的网络错误。各种Linux内核可以返回其他错误,例如ENOSR、ESOCKTNOSUPPORT、EPROTONOSUPPORT、ETIMEDOUT。在跟踪期间可以看到值ERESTARTSYS。

  • EMFILE :已达到打开的文件描述符数的每个进程限制

  • ENFILE :已达到系统范围内打开文件总数的限制

  • ENOBUFS, ENOMEM:没有足够的可用内存。这通常意味着内存分配受到套接字缓冲区限制,而不是系统内存的限制

  • ENOTSOCK sockfd不是套接字

  • EOPNOTSUPP 引用的套接字不是SOCK_STREAM类型

  • EPROTO :协议错误

  • EPERM (Linux) :防火墙规则禁止连接

我们简单地进行一次TCP对答通信的实现

server.c

/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  server.c
#    Created  :  Sat 21 Mar 2020 04:43:39 PM CST
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct _info {
    char name[10];
    char text[54];
}info;

int main(int argc, char *argv[])
{
    int my_socket;
    unsigned int len;
    int ret;

    // 创建套接字
    my_socket = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP socket
    if(my_socket == -1) { perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);

    // 用于接收消息
    info buf ={0};

    // 指定地址
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;  // 地址协议族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定端口号

    // 服务器 绑定
    bind(my_socket, (struct sockaddr *)&addr, sizeof(addr));

    // my_socket 只用于监听
    ret = listen(my_socket, 10);
    if(-1 == ret) { perror("listen"); }
    printf("Listening\n");

    int new_socket;
    struct sockaddr_in new = {0};
    int new_addr_size;
    // accept以后会返回一个新的套接字,用于与客户端通信
    new_socket = accept(my_socket, (struct sockaddr*)&new, &new_addr_size);
    printf("New socket is %d\n", new_socket);
    perror("accept");

    // 接收并打印消息
    //recvfrom(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
    recv(new_socket, &buf, sizeof(buf), 0);
        perror("recvfrom");

    printf("%s: %s\n", buf.name, buf.text);

    // 回复消息
    sprintf(buf.name, "Server");
    sprintf(buf.text, "Had recvied your message");
    //sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
    send(new_socket, &buf, sizeof(buf), 0);
    perror("sendto");

    // 关闭连接
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    close(new_socket); perror("close");
    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}

client.c

/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  client.c
#    Created  :  Sat 21 Mar 2020 04:43:39 PM CST
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

typedef struct _info {
    char name[10];
    char text[54];
}info;

int main(int argc, char *argv[])
{
    int my_socket;
    unsigned int len;
    int ret;

    // 创建套接字
    my_socket = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP socket
    if(my_socket == -1) { perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);

    // 用于接收消息
    info buf ={0};

    // 指定地址
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;  // 地址协议族
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
    addr.sin_port = htons(12345); //指定端口号

    // 用于连接服务器
    connect(my_socket, (struct sockaddr *)(&addr), sizeof(struct sockaddr_in));
    if(-1 == ret) { perror("connect"); }
    printf("connected\n");

    // 回复消息
    sprintf(buf.name, "Client");
    sprintf(buf.text, "Hello tcp text.");
    //sendto(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
    send(my_socket, &buf, sizeof(buf), 0);
    perror("sendto");

    // 接收并打印消息
    //recvfrom(my_socket, &buf, sizeof(buf), 0, NULL, NULL);
    recv(my_socket, &buf, sizeof(buf), 0);
    perror("recvfrom");

    printf("%s: %s\n", buf.name, buf.text);

    // 关闭连接
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");

    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}