TCP+IPv4 客户端 & 服务端 程序简例

97 篇文章 7 订阅
30 篇文章 1 订阅

TCP+IPv4 客户端 & 服务端 程序简例

本文给出基于IPv4的TCP客户端以及服务端程序样例。

服务端:

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

#define BACKLOG 16

int listen_fd = -1;

void sigINT(int signo);

int main()
{
    if (signal(SIGINT, sigINT) == SIG_ERR)
    {
        printf("set signal handler(SIGINT) error!!!\n");
        exit(1);
    }

    // socket
    if ( (listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
    {
        printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }

    // bind
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET; // IPv4
    server_addr.sin_port = htons(12500); // Port
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP
    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        printf("socket bind error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }

    // listen
    if (listen(listen_fd, BACKLOG) < 0)
    {
        printf("socket listen error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }

    printf("server init ok, start to accept new connect...\n");

    while (1)
    {
        // accept
        int client_fd = accept(listen_fd, NULL, NULL);
        if (client_fd < 0)
        {
            printf("socket accept error=%d(%s)!!!\n", errno, strerror(errno));
            exit(1);
        }
        printf("accept one new connect(%d)!!!\n", client_fd);

        static const char *msg = "Hello, Client!\n";
        if (write(client_fd, msg, strlen(msg)) != strlen(msg))
        {
            printf("send msg to client error!!!\n");
        }
        close(client_fd);
    }
}

void sigINT(int signo)
{
    printf("catch SIGINT, quit...\n");
    close(listen_fd);
    exit(0);
}

客户端:

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

int main()
{
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_fd < 0)
    {
        printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12500);
    if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0)
    {
        printf("inet_pton error!!!\n");
        exit(1);
    }

    if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        printf("socket connect error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }
    printf("connect to server ok!\n");

    char msg[1024];
    int rbytes = -1;
    while ( (rbytes = read(client_fd, msg, sizeof(msg)-1)) > 0)
    {
        msg[rbytes] = 0; // null terminate
        printf("%s\n", msg);
    }
    if (rbytes < 0)
    {
        printf("read error=%d(%s)!!!\n", errno, strerror(errno));
    }
    exit(0);
}

服务端注意事项:

一、server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

INADDR_ANY:在服务端套接字listen前,要先进行bind,也就是将一个确定的服务端的“协议地址”与服务端listen套接字绑定。

通常“协议地址”包含三部分:协议族、IP以及PORT(服务端口)。

其中INADDR_ANY是用来设置“协议地址”中IP部分。

若协议地址的IP部分设置为INADDR_ANY,并且服务端所在主机有多个网络接口(多个IP),则服务端可以在任意网络接口(任意IP)上接收客户端链接。

例如服务端所在的主机有三个IP:

10.1.1.1
10.1.1.2
10.1.1.3

如果设置“协议地址”的IP部分为INADDR_ANY(PORT=12500),则:
1)有客户端链接10.1.1.1:12500,服务端可以收到SYN包并建立链接;
2)有客户端链接10.1.1.2:12500,服务端可以收到SYN包并建立链接;
3)有客户端链接10.1.1.3:12500,服务端可以收到SYN包并建立链接;

如果设置“协议地址”的IP部分为10.1.1.3(PORT=12500),则:
1)有客户端链接10.1.1.1:12500,服务端不可以收到SYN包并建立链接;
2)有客户端链接10.1.1.2:12500,服务端不可以收到SYN包并建立链接;
3)有客户端链接10.1.1.3:12500,服务端可以收到SYN包并建立链接;

二、服务端accept到的客户端sockfd都为4

运行服务端程序并且运行三次客户端程序,可以发现服务端有以下输出:

[jiang@localhost 0406]$ ./server 
server init ok, start to accept new connection...
accept one new connect(4)!!!
accept one new connect(4)!!!
accept one new connect(4)!!!
^Ccatch SIGINT, quit...

为啥每次accept返回的客户端的sockfd都相同呢?

首先服务端启动时,fd已经有三个(0=标准输入;1=标准输出;2=标准错误),然后通过socket接口创建了listen_fd(3)。

然后通过accept,从已完成三次握手的队列中取出一个新的客户端链接,创建一个fd,此fd即为4。

服务端每次的行为都一样,先accept生成fd(4),然后写入数据到fd=4中,然后close(fd=4)。

在close后,当前服务端进程中的fd只剩下4个(0、1、2、3),fd=4又变为可用项。

当服务端再次accept生成新的fd时,内核总是从最低可用的fd开始创建占用,由于当前服务端进程有四个fd有各自用途,自然从fd=4重新开始。

每次都是close(fd=4),释放fd=4的状态为可用fd,然后accept时创建fd=4,指代新的客户端socket,往复循环。

客户端注意事项:

三、客户端read时可否不用循环读取?

在客户端connect链接服务端成功后,开始从中read数据,直到read不大于0。

read返回值:

1)>0:读到返回值大小的字节数据;
2)=0:对端关闭连接;
3)<0:read出现错误;

由于TCP是一种“数据流”,服务端写数据,客户端读数据,那客户端怎么知道啥时候“服务端要发送的全发送完毕啦!”这个信息呢?

我们在这里约定,当服务端将要发送的数据全部发送完毕时,就close客户端,通知客户端:我(服务端)要发送给你(客户端)的数据已经发送完毕啦!客户端read到0,得到了这个信息,于是乎就不再(能)继续读。当然,可以由客户端不断从收到的数据中检查某些约定好的“协议控制字符(组)”(这里的协议控制字符可以是任何字符,并非传统意义上的控制字符),例如,约定读取到两个\n就算做一个记录已经被读取完毕,开始下一个记录…即:人为约定、界定一条有效数据(或称为一条记录)的开始和结束。

更何况,万一在read时有信号来了,打断了read调用,导致只从内核接收缓冲区中读取了部分字节到用户态,仅仅调用一次read函数,不管缓冲区中是否有剩余数据,岂不是会导致数据丢失?

其实到这里已经有点偏离主题了…本意是给出一份客户端、服务端关于Socket API的使用样例,但是…

我上面的程序距离真正可用、能用、实用还有很多路要走,存在有很多可优化的地方,比如“SO_REUSEADDR ”等…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值