基于tcp的服务器端/客户端

详细的内容看这里,我就不复制了:

https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/README.md

TCP/IP 协议栈共分为 4 层,可以理解为数据收发分成了 4 个层次化过程,通过层次化的方式来解决问题

  • 链路层: 物理层,负责网络设备之间的通信,如网卡、网线等
  • IP层: 数据包进行中线选择
  • TCP/UDP层: 负责数据包的传输,保证数据包的可靠性和顺序性

实现基于 TCP 的服务器/客户端

代码:

hello_server

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock;
    int clnt_sock;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    char message[] = "Hello World!";

    if (argc != 2)
    {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
    //调用 socket 函数创建套接字
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));
    //调用 bind 函数分配ip地址和端口号
    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");
    //调用 listen 函数将套接字转为可接受连接状态
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    clnt_addr_size = sizeof(clnt_addr);
    //调用 accept 函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
        error_handling("accept() error");
    //稍后要将介绍的 write 函数用于传输数据,若程序经过 accept 这一行执行到本行,则说明已经有了连接请求
    write(clnt_sock, message, sizeof(message));
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

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

代码: hello_client

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);

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

    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
    //创建套接字,此时套接字并不马上分为服务端和客户端。如果紧接着调用 bind,listen 函数,将成为服务器套接字
    //如果调用 connect 函数,将成为客户端套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("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]));
    //调用 connect 函数向服务器发送连接请求
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!");

    str_len = read(sock, message, sizeof(message) - 1);
    if (str_len == -1)
        error_handling("read() error!");

    printf("Message from server : %s \n", message);
    close(sock);
    return 0;
}

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

echo服务器/客户端

程序运行的基本方式:

  • 服务器端在同一时刻只与一个客户端相连,并提供回声服务。
  • 服务器端依次向 5 个客户端提供服务并退出。for循环5次
  • 客户端接受用户输入的字符串并发送到服务器端。
  • 服务器端将接受的字符串数据传回客户端,即「回声」
  • 服务器端与客户端之间的字符串回声一直执行到客户端输入 Q 为止。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char message[BUF_SIZE];
    int str_len, i;

    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;

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

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error");

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

    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");

    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    clnt_adr_sz = sizeof(clnt_adr);
    //调用 5 次 accept 函数,共为 5 个客户端提供服务
    for (i = 0; i < 5; i++)
    {
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
        if (clnt_sock == -1)
            error_handling("accept() error");
        else
            printf("Connect client %d \n", i + 1);

        while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
            write(clnt_sock, message, str_len);

        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

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

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

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("socket() error");

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

    if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error!");
    else
        puts("Connected...........");

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

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

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

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

FAQ

问: 我在在linux下使用socket进行tcp网络编程,如果client端调用了close(sock)函数,那么server端口的read函数会读取到什么?

答:

在Linux下使用socket进行TCP网络编程时,如果客户端调用了close(sock)函数,服务器端的read函数的行为取决于客户端关闭连接的方式和时机。

通常情况下,当客户端调用close(sock)函数时,会向服务器发送一个FIN包,表示客户端想要关闭连接。服务器收到FIN包后,会做出以下响应:

  1. 如果服务器已经读取了所有客户端发送的数据,那么read函数会返回0,表示已经到达文件末尾(EOF)。这意味着客户端已经关闭了连接,并且没有更多的数据可读。

  2. 如果服务器还没有读取完客户端发送的所有数据,那么read函数会继续读取剩余的数据,直到读取完所有数据或者遇到错误为止。在这种情况下,read函数会返回实际读取到的字节数。当所有数据读取完毕后,再次调用read函数会返回0,表示已经到达文件末尾(EOF)。

  3. 如果客户端在发送数据的过程中突然关闭了连接,那么服务器的read函数可能会返回一个错误,通常是ECONNRESET错误,表示连接被重置。

需要注意的是,如果客户端在关闭连接之前没有发送所有的数据,那么服务器可能会丢失一部分数据。为了确保数据的完整性,通常建议在关闭连接之前,先调用shutdown(sock, SHUT_WR)函数,告诉操作系统不再发送数据,然后再调用close(sock)函数关闭连接。

总之,服务器端的read函数在客户端关闭连接时的行为取决于具体的情况:

  • 如果客户端发送了所有数据并关闭连接,read函数会返回0,表示EOF。
  • 如果客户端关闭连接时还有未读取的数据,read函数会继续读取剩余数据,直到读取完毕,然后返回0,表示EOF。
  • 如果客户端异常关闭连接,read函数可能会返回错误,如ECONNRESET

作为服务器端,需要正确处理这些不同的情况,以确保程序的正确性和鲁棒性。

问: 如何检测到 read函数返回一个错误, 例如是ECONNRESET?

在Linux系统中,当read函数返回错误时,会设置全局变量errno来指示具体的错误类型。你可以通过检查errno的值来判断是否发生了ECONNRESET错误。

下面是一个示例代码,演示了如何检测read函数返回的错误:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

ssize_t read_data(int sockfd, void *buf, size_t len) {
    ssize_t num_read = read(sockfd, buf, len);
    if (num_read == -1) {
        if (errno == ECONNRESET) {
            printf("Connection reset by peer\n");
        } else {
            perror("read");
        }
        return -1;
    }
    return num_read;
}

在上面的代码中,read_data函数封装了read函数的调用。它检查read函数的返回值:

  • 如果返回值为-1,表示发生了错误。此时,通过检查errno的值来判断具体的错误类型:
    • 如果errno的值等于ECONNRESET,表示连接被对端重置,可以输出相应的错误信息。
    • 否则,可以使用perror函数输出错误信息,perror函数会根据errno的值自动生成对应的错误描述。
  • 如果返回值不为-1,表示读取成功,函数返回实际读取到的字节数。

在实际使用时,你可以根据需要对read_data函数进行调用,并根据返回值进行相应的处理:

char buffer[1024];
ssize_t num_read = read_data(sockfd, buffer, sizeof(buffer));
if (num_read == -1) {
    // 处理错误情况
} else if (num_read == 0) {
    // 处理连接关闭的情况
} else {
    // 处理成功读取数据的情况
}

通过检查read_data函数的返回值,你可以判断是否发生了错误,并根据具体的错误类型采取相应的处理措施。

总之,通过检查read函数的返回值和errno的值,你可以检测并处理read函数返回的错误,包括ECONNRESET错误。这样可以增强程序的健壮性和可靠性。