三:基于TCP的服务端/客户端

这篇具有很好参考价值的文章主要介绍了三:基于TCP的服务端/客户端。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1 理解TCP和UDP

  • 其中四层模型说的就是TCP(UDP)/IP协议栈
并列图片
三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议
三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议

2 补充函数解释

三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议
  • listen详解
#include <sys/socket.h>

/**
* @param[1]: 希望进入等待状态的套接字
* @param[2]: 连接请求等待队列的长度
*/
int listen(int sock, int backlog);

当客户端发送连接请求时,并不一定能立即连接到。尽管此时服务端处于等待连接请求状态(listen),但是由于系统正忙,此连接请求需要进入连接请求等待队列,listen第二个参数便是设置此等待队列的大小。

  • accept函数详解
#include <sys/socket.h>

/**
* @brief : 受理连接请求等待队列中待处理的连接请求。
* @param[1] : sock:服务器套接字的文件描述符;
* @param[2] : addr:用于保存发起连接请求的客户端地址信息;
* @param[3] : addrlen:第二个参数的长度。
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);                                  
  • accept 函数会受理连接请求等待队列中待处理的客户端连接请求,它从等待队列中取出 1 个连接请求,创建套接字并完成连接请求。如果等待队列为空,accpet 函数会阻塞,直到队列中出现新的连接请求才会返回。
  • 它会在内部产生一个新的套接字并返回其文件描述符,该套接字用于与客户端建立连接并进行数据 I/O。新的套接字是在 accept 函数内部自动创建的,并自动与发起连接请求的客户端建立连接。
    accept 执行完毕后会将它所受理的连接请求对应的客户端地址信息存储到第二个参数 addr 中。
  • connect() 函数详解
#include <sys/socket.h>

/**
* @brief : 请求连接。
* @param[2] : 保存目标服务器端地址信息的结构体指针
*/
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);  

客户端调用 connect 函数后会阻塞,直到发生以下情况之一才会返回:

  1. 服务器端接收连接请求。
  2. 发生断网等异常情况而中断连接请求。

注意:上面说的”接收连接请求“并不是服务器端调用了 accept 函数,而是服务器端把连接请求信息记录到等待队列。因此 connect 函数返回后并不立即进行数据交换。

3 实现迭代回声服务端和客户端(问题版)

迭代回声服务器端与回声客户端的基本运行方式:

  1. 服务器端同一时刻只与一个客户端相连接,并提供回声服务。
  2. 服务器端依次向 5 个客户端提供服务,然后退出。
  3. 客户端接收用户输入的字符串并发送到服务器端。
  4. 服务器端将接收到的字符串数据传回客户端,即”回声“。
  5. 服务器端与客户端之间的字符串回声一直执行到客户端输入 Q 为止。
    三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议
    三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议

服务端代码

注:由于不同公司代码规范的规定不同,所以书写方式可能跟书本有出入,不过不影响内容一致

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

const int kBufferSize = 1024;

void ErrorHandling(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char* argv[]) {
    int server_sock;
    int client_sock;
    char message[kBufferSize];
    int str_len = 0;
    
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    socklen_t client_addr_size;

    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    server_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (server_sock == -1) {
        ErrorHandling("socket() error");
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));

    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        ErrorHandling("bind() error");
    }

    if (listen(server_sock, 5) == -1) {
        ErrorHandling("listen() error");
    }

    client_addr_size = sizeof(client_addr);

    //循环变量i C11可以写里面了
    for (int i = 0; i < 5; i++) {
        client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_size);
        if (client_sock == -1) {
            ErrorHandling("accept() error");
        } else {
            printf("connected client %d \n", i + 1);
        }

        while ((str_len = read(client_sock, message, kBufferSize)) != 0) {
            write(client_sock, message, str_len);
        }

        close(client_sock);
    }

    close(server_sock);
    return 0;
}

客户端代码

提前说明存在的问题:
在本章的回声客户端的实现中有上面这段代码,它有一个错误假设:每次调用 read、write 函数时都会执行实际的 I/O 操作。
但是注意:TCP 是面向连接的字节流传输,不存在数据边界。所以多次 write 的内容可能一直存放在发送缓存中,某个时刻再一次性全都传递到服务器端,这样的话客户端前几次 read 都不会读取到内容,最后才会一次性收到前面多次 write 的内容。还有一种情况是服务器端收到的数据太大,只能将其分成多个数据包发送给客户端,然后客户端可能在尚未收到全部数据包时旧调用 read 函数。
理解:问题的核心在于 write 函数实际上是把数据写到了发送缓存中,而 read 函数是从接收缓存读取数据。并不是直接对 TCP 连接的另一方进行数据读写。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

const int kBufferSize = 1024;

void ErrHandling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char *argv[]) {
    int sock;
    char message[kBufferSize];
    int str_len;
    struct sockaddr_in serv_addr;

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
        ErrHandling("socket() error");
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        ErrHandling("connect() error");
    } else {
        puts("Connected...........");
    }


    while (1) {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, kBufferSize, stdin);

        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
            break;
        }

        write(sock, message, strlen(message));
        str_len = read(sock, message, kBufferSize - 1);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
    close(sock);
    return 0;
}

基于win的实现

将 Linux 平台下的实例转化为 Windows 下的实例,记住以下四点:

  1. 通过 WSAStartup、WSACleanup 函数初始化并清除套接字相关库。
  2. 把数据类型和变量名切换为 Windows 风格。
  3. 数据传输中用 recv、send 函数而非 read、write 函数。
  4. 关闭套接字时用 closesocket 函数而非 close 函数。

4 迭代回声服务端/客户端(优化)

三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议
由于write和read都是有缓冲的,所以写完直接只读一次,可能读不全。注意:一定能读到,因为read是阻塞IO函数。所以为了读全,需要循环去读。

客户端代码优化

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

const int kBufferSize = 1024;

void ErrHandling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char *argv[]) {
    int sock;
    char message[kBufferSize];
    int recv_len = 0;
    int str_len;
    int recv_cnt = 0;
    struct sockaddr_in serv_addr;

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
        ErrHandling("socket() error");
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        ErrHandling("connect() error");
    } else {
        puts("Connected...........");
    }

    while (1) {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, kBufferSize, stdin);

        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
            break;
        }

        str_len = write(sock, message, strlen(message)); //发送的字节数
        recv_len = 0;

        while (recv_len < str_len) {
            recv_cnt = read(sock, message, kBufferSize - 1);
            if (recv_cnt == -1) {
                ErrHandling("read() error");
            }
            recv_len += recv_cnt;
        }
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
    close(sock);
    return 0;
}

新问题:如果不知道接收的数据的长度

上面的回声客户端中,提前就知道要接收的数据长度,但是一般是不知道的。这种情况下,要解决拆包和粘包的问题,就要定义应用层协议。
应用层协议实际就是在服务器端/客户端的实现过程中逐步定义的规则的集合。
在应用层协议中可以定好数据边界的表示方法、数据的长度范围等。

代码体验定义应用层协议

程序描述如下

服务端从客户端获取多个数字和运算符信息。服务端搜索到数字后进行运算,将结果传回客户端。例如给服务端发送3、5、9、* 则服务端返回359的结果

我们定义的协议如下:

  1. 客户端用 1 个字节整数形式传递操作数的个数。
  2. 客户端向服务器端传送的每个操作数占用 4 字节。
  3. 传递完操作数后紧跟着传递一个占用 1 字节的运算符。
  4. 服务器端以 4 字节整数向客户端传回运算结果。
  5. 客户端得到运算结果后终止与服务器端的连接。

先展示效果
三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议

自己瞎写写

  • op_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

const int kBufferSize = 1024;

void ErrHandling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int Calculate(int op_num, int *op_param, char op_character)
{
    int res = op_param[0];
    switch (op_character) {
    case '+':
        for (int i = 1; i < op_num; i++) {
            res += op_param[i];
        }
        break;
    case '-':
        for (int i = 1; i < op_num; i++) {
            res -= op_param[i];
        }
        break;
    case '*':
        for (int i = 1; i < op_num; i++) {
            res *= op_param[i];
        }
        break;
    default:
        return 0;
    }

    return res;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    int server_sock = -1;
    int client_sock = -1;

    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    
    //1
    server_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (server_sock == -1) {
        ErrHandling("socket() error");
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));

    //2:
    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        ErrHandling("bind() error");
    }

    //3:
    if (listen(server_sock, 5) == -1) {
        ErrHandling("listen() error");
    }

    //4: 此处就处理一个连接,如果想处理多个连接,将1改大点
    for (int i = 0; i < 1; i++) {
        client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_sock == -1) {
            ErrHandling("accept() error");
        }

        int op_num = 0;
        read(client_sock, &op_num, 1); //读取一个字节

        //接下来接收,4*op_num + 1个字节的数据
        int recv_len = 0;
        int recv_cnt = 0;
        char op_data[kBufferSize];
        while (recv_len < (4*op_num + 1)) {
        	//不断更新数据应该写入的位置
            recv_cnt = read(client_sock, &op_data[recv_len], kBufferSize -1); 
            recv_len += recv_cnt;
        }
        int res = Calculate(op_num, (int*)op_data, op_data[recv_len - 1]);
        write(client_sock, (void*)&res, sizeof(res));
        close(client_sock);
    }
    close(server_sock);
    return 0;
}
  • op_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

const int kBufferSize = 1024;

void ErrHandling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char *argv[]) {
    int sock;
    char message[kBufferSize];
    int recv_len = 0;
    int str_len;
    int recv_cnt = 0;
    struct sockaddr_in serv_addr;

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    //1st
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
        ErrHandling("socket() error");
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    //2nd
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        ErrHandling("connect() error");
    } else {
        puts("Connected...........");
    }

    char msg[kBufferSize]; //存放要发的数据

    fputs("Operand count: ", stdout);
    int op_cnt = 0;
    scanf("%d", &op_cnt);
    msg[0] = (char)op_cnt;

    for (int i = 0; i < op_cnt; i++) {
        printf("Operand %d: ", i + 1);
        scanf("%d", (int*)&msg[i*4 + 1]);
    }
    //删除缓冲区中的\n,因为下边要输入字符。这里看结尾的链接就行,好久不写C,我一开始也没注意
    fgetc(stdin); 
    fputs("Operator: ", stdout);
    scanf("%c", &msg[op_cnt * 4 + 1]);
    write(sock, msg, op_cnt*4 + 2);
    int res;
    read(sock, &res, 4);

    printf("Message from server: %d", res);

    close(sock);
    return 0;
}

链接: 为什么scanf之前要使用fgetc接受一个字符

5 TCP原理

TCP套接字中的IO缓冲

如前所述,TCP套接字的数据收发无边界,所以数据不是发了就一定能立即全部收到。
Actually,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据,都要经过缓冲区。
三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议
套接字 I/O 缓冲的特性:

  • I/O 缓冲在每个套接字中单独存在。
  • I/O 缓冲在创建套接字时自动生成。
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
  • 关闭套接字将丢失输入缓冲中的数据。

注:不会发生超过输入缓冲区大小的数据传输,即:服务器发多少数据是跟客户端询问过的。

6 基于windows的实现

https://gitee.com/pipe-man/tcp_ip_socket/tree/master/%E6%BA%90%E4%BB%A3%E7%A0%81
自取吧,懒得看Windows了,工作用不到win,用到再补。
三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议文章来源地址https://www.toymoban.com/news/detail-808393.html

到了这里,关于三:基于TCP的服务端/客户端的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Socket网络编程(TCP/IP)实现服务器/客户端通信。

    一.前言 回顾之前进程间通信(无名管道,有名管道,消息队列,共享内存,信号,信号量),都是在同一主机由内核来完成的通信。 那不同主机间该怎么通信呢? 可以使用Socket编程来实现。 Socket编程可以通过网络来实现实现不同主机之间的通讯。 二.Socket编程的网络模型如

    2024年02月08日
    浏览(65)
  • 【网络编程】——基于TCP协议实现回显服务器及客户端

    个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【网络编程】【Java系列】 本专栏旨在分享学习网络编程的一点学习心得,欢迎大家在评论区交流讨论💌 TCP提供的API主要有两个类 Socket ( 既会给服务器使用也会给客

    2024年02月03日
    浏览(47)
  • 网络编程:编写一个TCP客户端与服务端

    用的系统是Ubuntu。 socket用来创建套接字。这个函数服务端与客户端都要使用。 第一个参数用来制定地址族规范,比如 AF_INET(PF_INET) 表示IPv4地址, AF_INET6(PF_INET6) 表示IPv6地址。 第二个参数用来制定套接字的类型规范,如 SOCK_STREAM 表示面向连接的套接字, SOCK_DGRAM 表示面

    2024年02月01日
    浏览(34)
  • 【网络编程】实现UDP/TCP客户端、服务器

    需要云服务器等云产品来学习Linux的同学可以移步/--腾讯云--/--阿里云--/--华为云--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。   目录 一、UDP 1、Linux客户端、服务器 1.1udpServer.hpp 1.2udpServer.cc 1.3udpClient.hpp 1.4udpClient.cc 1.5onlineUser.hpp 2、Windows客户端 二、T

    2024年02月06日
    浏览(43)
  • 网络编程——socket服务端和客户端(TCP)

    所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通

    2024年02月07日
    浏览(32)
  • 【Linux | 网络编程】TCP的服务端(守护进程) + 客户端

    上一节,我们用了udp写了一个服务端和客户端之间通信的代码,只要函数了解认识到位,上手编写是很容易的。 本章我们开始编写tcp的服务端和客户端之前通信的代码,要认识一批新的接口,并将我们之前学习的系统知识加进来,做到融会贯通… 代码详情:👉 Gitee 对于TC

    2024年01月16日
    浏览(31)
  • C#实现简单TCP服务器和客户端网络编程

    在C#中进行网络编程涉及许多类和命名空间,用于创建和管理网络连接、传输数据等。下面是一些主要涉及的类和命名空间: System.Net 命名空间: 这个命名空间提供了大部分网络编程所需的类,包括: IPAddress :用于表示IP地址。 IPEndPoint :表示IP地址和端口号的组合。 Socke

    2024年02月11日
    浏览(45)
  • Python网络编程实战:构建TCP服务器与客户端

    Python网络编程实战:构建TCP服务器与客户端 在信息化时代,网络编程是软件开发中不可或缺的一部分。Python作为一种功能强大的编程语言,提供了丰富的网络编程库和工具,使得开发者能够轻松构建各种网络应用。本文将详细介绍如何在Python中进行网络编程,特别是如何使用

    2024年04月15日
    浏览(30)
  • python网络编程:通过socket实现TCP客户端和服务端

    目录 写在开头 socket服务端(基础) socket客户端(基础) 服务端实现(可连接多个客户端)  客户端实现 数据收发效果   近期可能会用python实现一些网络安全工具,涉及到许多关于网络的知识,逃不过的就是最基本的socket。本文将介绍如何通过python自带的socket库实现TCP客户

    2024年03月21日
    浏览(40)
  • Socket实例,实现多个客户端连接同一个服务端代码&TCP网络编程 ServerSocket和Socket实现多客户端聊天

    Java socket(套接字)通常也称作\\\"套接字\\\",用于描述ip地址和端口,是一个通信链的句柄。应用程序通常通过\\\"套接字\\\"向网络发出请求或者应答网络请求。 使用socket实现多个客户端和同一客户端通讯;首先客户端连接服务端发送一条消息,服务端接收到消息后进行处理,完成后再

    2024年02月12日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包