Linux c编程之TCP通信

一、说明

TCP(Transmission Control Protocol),由RFC 793定义,中文名为传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP是常用的网络传输协议之一,该协议是面向连接、可靠传输的字节流协议。在Linux C网络程序中广泛使用,如http/https、ftp等。
TCP通信分为客户端和服务端,传输数据前需要客户端向服务端发起建立连接,数据传输完成后,双方可以断开连接。

二、常用API介绍

2.1 socket()

   #include <sys/types.h>
   #include <sys/socket.h>

   int socket(int domain, int type, int protocol);

作用: 用来创建一个通信的终端实例
参数说明:
  domain: 协议族,用AF_INET表示IPv4
  type: 传输方式,常用的有以下两种
    SOCK_STREAM: TCP
    SOCK_DGRAM: UDP
  protol: 特殊协议,实际应用中都是写为0
返回值:
  成功时返回一个socket文件描述符,失败时返回-1,errno会被设置,可以通过errno值获取错误码

2.2 bind()

   #include <sys/types.h>
   #include <sys/socket.h>

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

作用:绑定网络地址(IP/PORT)到socket
参数说明:
  sockfd: socket()返回的描述符
  addr: 绑定的地址
  addrlen: 绑定的地址结构长度
返回值:
  成功时返回0,失败时返回-1,errno会被设置,可以通过errno值获取错误码

2.3 connect()

   #include <sys/types.h>      
   #include <sys/socket.h>

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

作用:连接到服务端
参数说明:
  sockfd: socket()返回的描述符
  addr: 服务端的地址
  addrlen: 服务端的地址结构长度
返回值:
  成功时返回0,失败时返回-1,errno会被设置,可以通过errno值获取错误码

2.4 listen()

   #include <sys/types.h>
   #include <sys/socket.h>

   int listen(int sockfd, int backlog);

作用:监听客户端的连接请求
参数说明:
  sockfd: socket()返回的描述符
  backlog: 待处理的客户端连接请求的队列最大长度,如果待处理的队列满了,则收到客户端请求会返回
返回值:
  成功时返回0,失败时返回-1,errno会被设置,可以通过errno值获取错误码

2.5 accept()

   #include <sys/types.h>
   #include <sys/socket.h>

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

作用:接受客户端的连接请求
参数说明:
  sockfd: socket()返回的描述符
  addr: 服务端的地址
  addrlen: 服务端的地址结构长度
返回值:
  成功时返回对应客户端连接的socket描述符,失败时返回-1,errno会被设置,可以通过errno值获取错误码

2.6 read()和write()

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

作用:
  可以通过系统的write/read函数发送/接收网络数据,其中fd对应socket文件描述符
参数说明:
  fd: socket()返回的网络描述符
  buf:接收或发送缓冲区
  count:发送的字节数或接收的缓冲区大小
返回值:
  大于0时,表示读取或写入的字节数; -1表示错误

2.7 close()

#include <unistd.h>
int close(int fd);

作用:
  关闭一个描述符(文件/socket)

三、实际应用分析

3.1 基本TCP通信

server_tcp.c:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define SERVER_PORT 9999

int server_tcp()
{
    int ret = 0;
    int socket_fd = -1;
    int client_fd = -1;
    int addr_len = 0;    
    unsigned int value = 1;
    char buf[1024] = {0};
    int read_len = 0;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;

    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        printf("%s: socket failed\n", __FUNCTION__);
        goto on_error;
    }

    if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR,
            (void *)&value, sizeof(value)) < 0) {
        printf("%s: Fail to setsockopt\n", __FUNCTION__);
        goto on_error;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    addr_len = sizeof(server_addr);
    ret = bind(socket_fd, (const struct sockaddr *)&server_addr, addr_len);
    if (ret < 0) {
        printf("%s: bind failed\n", __FUNCTION__);
        goto on_error;
    }

    ret = listen(socket_fd, 5);
    if (ret < 0) {
        printf("%s: listen failed\n", __FUNCTION__);
        goto on_error;
    }

    client_fd = accept(socket_fd
            , (struct sockaddr *)&client_addr, &addr_len);
    if (client_fd < 0) {
        printf("%s: invalid client fd[%d]\n", __FUNCTION__, client_fd);
        return -1;
    }
    printf("client fd[%d] connect\n", client_fd);

    while (1) {
        read_len = read(client_fd, buf, 1024);
        if (read_len > 0) {
            printf("recv data[%s] from client\n", buf);
            write(client_fd, buf, read_len);
            printf("send data[%s] to client\n", buf);
        } else if (read_len < 0) {
            printf("errno:%d\n", errno);
            break;
        } else if (0 == read_len) {
            printf("client fd[%d] disconnect\n", client_fd);
            break;
        }
    }

    close(socket_fd);
    close(client_fd);

    return 0;

on_error:

    close(socket_fd);
    close(client_fd);

    return -1;
}

int main(int argc, char *argv[])
{
    int ret = -1;

    ret = server_tcp();
    if (ret < 0) {
        printf("%s: server tcp failed\n", __FUNCTION__);
        return -1;
    }

    return 0;
}

client_tcp.c:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9999

int client_tcp(char *server_ip, int server_port, char *data)
{
    int ret = 0;
    int socket_fd = -1;
    int addr_len = 0;
    char buf[1024] = {0};
    struct sockaddr_in server_addr;

    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        printf("%s: socket failed\n", __FUNCTION__);
        return 0;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip);

    addr_len = sizeof(server_addr);
    ret = connect(socket_fd, (struct sockaddr *)&server_addr, addr_len);
    if (ret < 0) {
        printf("%s: connect failed, errno:%d, %s\n", __FUNCTION__, errno, strerror(errno));
        goto on_error;
    }

    ret = write(socket_fd, data, strlen(data));
    printf("send data[%s] to server\n", data);

    ret = read(socket_fd, buf, sizeof(buf));
    if (ret > 0) {
        printf("recv data[%s] from server\n", buf);
    } else if (ret < 0) {
        printf("%s: errno:%d\n", __FUNCTION__, errno);
    } else if (0 == ret) {
        printf("server fd[%d] disconnect\n", socket_fd);
    }

    close(socket_fd);

    return 0;

on_error:

    close(socket_fd);

    return -1;
}

int main(int argc, char *argv[])
{
    if (argc < 4) {
        printf("usage: %s 127.0.0.1 9999 hello\n", argv[0]);
        return -1;
    }

    client_tcp(argv[1], atoi(argv[2]), argv[3]);

    return 0;
}

Makefile:

all:
	gcc -o server server_tcp.c
	gcc -o client client_tcp.c
clean:
	-@rm server client

编译:
make
运行:

$ ./server
client fd[4] connect
recv data[helloworld] from client
send data[helloworld] to client
client fd[4] disconnect

$ ./client 127.0.0.1 9999 helloworld
send data[helloworld] to server
recv data[helloworld] from server

服务端不启动时,客户端连接失败,如下:

$ ./client 127.0.0.1 9999 helloworld
client_tcp: connect failed, errno:111, Connection refused

服务端启动时,可以看见监听的端口,如下:

$ netstat -antp | grep server
tcp        0      0 0.0.0.0:9999            0.0.0.0:*               LISTEN      36467/server

已经有服务端启动时,再启动服务端bind()接口会失败:

$ ./server
bind failed, errno:98, Address already in use

3.2 服务端支持IO复用

为了使服务端能够多次处理连接,使用epoll函数管理socket描述符。
源码:
server_tcp.c:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>

#define SERVER_PORT 9999


int g_listen_fd = -1;
int g_epoll_fd = -1;

int server_tcp()
{
    int ret = 0;
    long socket_fd = -1;
    int addr_len = 0;    
    unsigned int value = 1;
    struct epoll_event event;
    struct sockaddr_in server_addr;

    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        printf("%s: socket failed\n", __FUNCTION__);
        goto on_error;
    }

    if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR,
            (void *)&value, sizeof(value)) < 0) {
        printf("%s: Fail to setsockopt\n", __FUNCTION__);
        goto on_error;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    addr_len = sizeof(server_addr);
    ret = bind(socket_fd, (const struct sockaddr *)&server_addr, addr_len);
    if (ret < 0) {
        printf("%s: bind failed\n", __FUNCTION__);
        goto on_error;
    }

    ret = listen(socket_fd, 5);
    if (ret < 0) {
        printf("%s: listen failed\n", __FUNCTION__);
        goto on_error;
    }

    event.events =  EPOLLIN;
    event.data.fd = socket_fd;
    ret = epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, socket_fd, &event);
    if(ret < 0) {
        printf("epoll_ctl failure:%s\n", strerror(errno));
        close(g_epoll_fd);
        return -1;
    }

    g_listen_fd = socket_fd;

    return 0;

on_error:

    close(socket_fd);

    return -1;
}

int main(int argc, char *argv[])
{
    int ret = -1;
    int num = 0;
    int i = 0;
    int client_fd = 0;
    int addr_len = 0;
    int read_len = 0;
    char buf[1024] = {0};
    struct epoll_event event;
    struct sockaddr_in client_addr;
    struct epoll_event events_array[10];

    g_epoll_fd = epoll_create(10);
    if (g_epoll_fd < 0) {
        printf("epoll_create failure:%s\n", strerror(errno));
        return -1;
    }

    ret = server_tcp();
    if (ret < 0) {
        printf("%s: server tcp failed\n", __FUNCTION__);
        return -1;
    }

    while (1) {
        num = epoll_wait(g_epoll_fd, events_array, 10, -1);
        if (num < 0) {
            printf("epoll_wait failure:%s\n", strerror(errno));
            close(g_epoll_fd);
            break;
        } else if (num == 0) {
            printf("eopll_wait timeout!\n");
            continue;
        }

        for (i = 0; i < num; i++) {
            if(events_array[i].events == EPOLLIN) {
                if (events_array[i].data.fd == g_listen_fd) {
                    client_fd = accept(events_array[i].data.fd,
                            (struct sockaddr *)&client_addr, &addr_len);
                    if (client_fd < 0) {
                        printf("%s: invalid client fd[%d]\n", __FUNCTION__, client_fd);
                        return -1;
                    }

                    printf("client %s:%d fd[%d] connect\n",
                                    inet_ntoa(client_addr.sin_addr),
                                    ntohs(client_addr.sin_port), client_fd);

                    event.events =  EPOLLIN;
                    event.data.fd = client_fd;
                    ret = epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
                    if(ret < 0) {
                        printf("epoll_ctl failure:%s\n", strerror(errno));
                        return -1;
                    }
                } else {
                    memset(buf, 0, sizeof(buf));
                    read_len = read(events_array[i].data.fd, buf, 1024);
                    if (read_len > 0) {
                        printf("recv data[%s] from client fd[%d]\n",
                                buf, events_array[i].data.fd);
                    } else if (read_len < 0) {
                        epoll_ctl(g_epoll_fd, EPOLL_CTL_DEL, events_array[i].data.fd, NULL);
                        close(events_array[i].data.fd);
                    } else if (0 == read_len) {
                        printf("client fd[%d] disconnect\n",
                                events_array[i].data.fd);
                        epoll_ctl(g_epoll_fd, EPOLL_CTL_DEL, events_array[i].data.fd, NULL);
                        close(events_array[i].data.fd);
                    }
                }
            }

        }
    }

    return 0;
}

client_tcp.c:

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

int client_tcp(char *server_ip, int server_port, char *data)
{
    int ret = 0;
    int socket_fd = -1;
    int addr_len = 0;
    struct sockaddr_in server_addr;

    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        printf("%s: socket failed\n", __FUNCTION__);
        return 0;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip);

    addr_len = sizeof(server_addr);
    ret = connect(socket_fd, (struct sockaddr *)&server_addr, addr_len);
    if (ret < 0) {
        printf("%s: connect failed\n", __FUNCTION__);
        goto on_error;
    }

    ret = write(socket_fd, data, strlen(data));

    close(socket_fd);

    return 0;

on_error:

    close(socket_fd);

    return -1;
}

int main(int argc, char *argv[])
{
    if (argc < 4) {
        printf("usage: %s 127.0.0.1 9999 hello\n", argv[0]);
        return -1;
    }

    client_tcp(argv[1], atoi(argv[2]), argv[3]);

    return 0;
}

Makefile:

all:
	gcc -o server server_tcp.c
	gcc -o client client_tcp.c
clean:
	-@rm server client

客户端端口变化问题

$  ./server
client 127.0.0.1:50280 fd[5] connect
recv data[hello] from client fd[5]
client fd[5] disconnect
client 127.0.0.1:50281 fd[5] connect
recv data[hello] from client fd[5]
client fd[5] disconnect
client 127.0.0.1:50282 fd[5] connect
recv data[hello] from client fd[5]
client fd[5] disconnect

客户端端口变化是由于没有绑定指定的端口,系统每次随机分配了一个端口。

memset(&client_addr, 0, sizeof(client_addr));                                                                                        
    client_addr.sin_family = AF_INET;
    client_addr.sin_port = htons(10000);
    client_addr.sin_addr.s_addr = inet_addr(127.0.0.1);
    addr_len = sizeof(client_addr);

    ret = bind(socket_fd, (const struct sockaddr *)&client_addr, addr_len);
    if (ret < 0) {
        printf("%s: bind failed\n", __FUNCTION__);
        close(socket_fd);
        return 0;
    }

测试:
多次启动客户端,均为同一个端口10000

$ ./server
client 127.0.0.1:10000 fd[5] connect
recv data[hello] from client fd[5]
client fd[5] disconnect
client 127.0.0.1:10000 fd[5] connect
recv data[hello] from client fd[5]
client fd[5] disconnect

###3.3 绑定失败问题

$ ./client 127.0.0.1 9999 hello
$ ./client 127.0.0.1 9999 hello
client_tcp: bind failed, errno:98, Address already in use

$ netstat -antp | grep 10000
tcp        0      0 127.0.0.1:10000         127.0.0.1:9999          TIME_WAIT

原因是之前的连接还没有完全释放,可以在调用bind()接口前将socket设置为地址可重复使用,对客户端、服务端同样适用。

socket_fd = socket(AF_INET, SOCK_STREAM, 0); 
    if (socket_fd < 0) {
        printf("%s: socket failed\n", __FUNCTION__);
        return 0;
    }   

    unsigned int value = 1;
    if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR,
            (void *)&value, sizeof(value)) < 0) {
        printf("%s: Fail to setsockopt\n", __FUNCTION__);
        goto on_error;
    }   

    memset(&client_addr, 0, sizeof(client_addr));
    client_addr.sin_family = AF_INET;
    client_addr.sin_port = htons(CLINET_PORT);
    client_addr.sin_addr.s_addr = inet_addr(CLINET_IP);

测试:

$ ./client 127.0.0.1 9999 hello
$ ./client 127.0.0.1 9999 hello
$ ./client 127.0.0.1 9999 hello

这时就没有绑定问题了

3.4 服务端监听地址问题

  1. server监听在本地回环地址127.0.0.1
$ ./server 127.0.0.1 9999
client 127.0.0.1:10000 fd[5] connect
recv data[hello] from client fd[5]
client fd[5] disconnect

在同一台机器,运行client可以访问

$ ./client 127.0.0.1 9999 hello

但是在另外一台机器,是连接不上的,因为127.0.0.1是本地回环地址,只能在同一台机器内使用,如下:

$ ./client 127.0.0.1 9999 hello
client_tcp: connect failed

因此,server端需要绑定其它机器可以访问的地址。比如server 端所在机器是192.168.0.29,可以绑定在192.168.0.29。
测试结果:

$ ./server 192.168.0.29 9999
client 192.168.0.132:10000 fd[5] connect
recv data[hello] from client fd[5]
client fd[5] disconnect
$ ./client 192.168.0.29 9999 hello

同时,同一台机器也可以通过127.0.0.1连接

$ ./server 192.168.0.29 9999
client 127.0.0.1:10000 fd[5] connect
recv data[hello] from client fd[5]
client fd[5] disconnect
$ ./client 192.168.0.29 9999 hello

但是在实际环境中,比如云服务器,主机上一般都有多块网上、多个IP,如本地回环地址127.0.0.1、局域网IP(也称私网地址)、公网IP等,如下某主机IP地址情况:
本地回环地址:127.0.0.1
私有IP:192.168.0.5, 192.168.1.5
公网地址:36.87.80.2,36.78.90.3

如果服务端监听在192.168.0.5地址,客户端192.168.0.x可以连接,但客户端192.168.1.x连接不上;如果服务端监听在192.168.1.5地址,客户端192.168.1.x可以连接,但客户端192.168.0.x连接不上。
有一种方法是同时监听需要监听的地址,常用于公网服务器。另一种方法是直接监听在0.0.0.0地址,即表示监听主机的所有IP地址,这也是非常常用的方法。

四、关键说明

  • TCP是常用的网络层传输协议,最经典的应用场景是HTTP协议
  • TCP通信有重传机制,是可靠传输
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪游东戴河

你就是这个世界的唯一

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值