alin的学习之路(Linux网络编程:二)(三次握手四次挥手、read函数返回值、错误函数封装、多进程高并发服务器)

alin的学习之路(Linux网络编程:二)(三次握手四次挥手、read函数返回值、错误函数封装、多进程高并发服务器)

1. 服务器获取客户端地址和端口号

accept函数会返回客户端的sockaddr,通过使用inet_ntop()ntohs()即可获取客户端地址和端口号

char clt_IP[1024];
clt_addr_len = sizeof(clt_addr);
cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);
printf("客户端ip:%s,port:%d接入服务器\n",
	inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,clt_IP,sizeof(clt_IP)),
    ntohs(clt_addr.sin_port));

2. TCP 通信时序

1. 三次握手

在这里插入图片描述

  • 注意:三次握手由客户端发出请求,而非服务器
  1. 主动发起连接请求端(客户端),发送 SYN 标志位,携带 序号

  2. 被动接收连接请求端(服务端),回复 ACK 标志位,携带 确认序号,并发送 SYN 标志位,携带序号

  3. 主动发起连接请求端(客户端),发送 ACK 标志位

    —— 当第二个 ACK 发送完成, 标志 3次握手完成。 连接建立成功。
    —— 服务器, accept() 成功返回。
    —— 客户端, connect() 成功返回。

SYN和序号 + ACK和确认序号表示该序号之前的数据包全部接收到,该序号和确认序号也就是TCP通信的安全之处

2. 四次挥手

在这里插入图片描述

  1. 主动关闭连接请求端(客户端),发送 FIN 标志位 , 携带序号。
  2. 被动关闭连接请求端(服务端),接收 FIN, 发送 ACK 标志位,携带 确认序号。
    —— 半关闭完成。
  3. 被动关闭连接请求端(服务端),发送 FIN 标志位 , 携带序号。
  4. 主动关闭连接请求端(客户端),接收 FIN, 发送 ACK 标志位,携带 确认序号。
    —— 最后一个 ACK 被 收到, 标志着 4次挥手完成。 TCP 连接关闭。

3. 半关闭

半关闭的实现, 依赖底层内核实现 socket 的原理。

半关闭关闭的是socket缓冲区,也就是关闭了数据的发送,但标志位等在TCP数据报格式的前面

4. 滑动窗口

通知通信的对端, 本端缓冲区的剩余空间大小(实时), 保证数据不会丢失。
滑动窗口 在 TCP 协议格式中存储, 上限 为 65536

在这里插入图片描述

3. read函数的返回值

  1. > 0 实际读到的字节数。
  2. = 0 已经读到结尾。(对端关闭)【重点】
  3. -1 应该进一步判断 errno 的值。
    errno == EAGAIN or EWOULDBLOCK : 设置了非阻塞方式读,但没有数据。
    errno == EINTR : 慢速系统调用,被信号中断。
    errno == “其他情况”。 异常。

4. 错误处理函数封装

因为在socket编程中,每一个函数的调用都需要检查返回值,并且所有的函数都加上返回值后使得代码变得冗余。

解决办法:可以自行封装函数,将函数名写为首字母大写,这样在vim中还可以使用K进行跳转

  • wrap.c
    • 存放 网络通信常用的 自定义函数实现。
    • 命名方式: 系统调用函数首字母大写, 方便跳转 man 手册。
    • 函数功能,添加系统调用的 出错场景。
    • 在 server.c / client.c 中, 调用自定义函数。
  • 编译:
    server.c 和 wrap.c 编译生成 server
    client.c 和 wrap.c 编译生成 client
  • wrap.h
    存放 网络通信常用的 自定义函数原型。

wrap.c

#include "wrap.h"

void sys_err(const char* str)
{
    perror(str);
    exit(1);
}

int Socket(int domain, int type, int protocol)
{
    int n = 0;

    n = socket(domain,type,protocol);
    if(n < 0)
        sys_err("socket error");

    return n;
}

int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
{
    int n = 0;

again:
    n = accept(sockfd,addr,addrlen);
    if(n < 0)
    {
        if(errno == EINTR || errno == ECONNABORTED)
            goto again;
        else
            sys_err("accept error");
    }

    return n;
}

int Bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen)
{
    int n = 0;

    n = bind(sockfd,addr,addrlen);
    if(n < 0)
        sys_err("bind error");

    return n;
}

int Listen(int sockfd, int backlog)
{
    int n = 0;

    n = listen(sockfd,backlog);
    if(n < 0)
        sys_err("listen error");

    return n;
}

int Connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen)
{
    int n = 0;

    n = connect(sockfd,addr,addrlen);
    if(n < 0)
        sys_err("connect error");

    return n;
}

ssize_t Read(int fd, void *buf, size_t count)
{
    ssize_t n = 0;
again:
    n = read(fd,buf,count);
    if(n < 0)
    {
        if(errno == EINTR)
            goto again;
        else
            sys_err("read error");
    }

    return n;
}

ssize_t Write(int fd, const void *buf, size_t count)
{
    ssize_t n = 0;
again:
    n = write(fd,buf,count);
    if(n < 0)
    {
        if(errno == EINTR)
            goto again;
        else
            sys_err("write error");
    }

    return n;
}

int Close(int fd)
{
    int n = 0;

    n = close(fd);
    if(n < 0)
        sys_err("close error");

    return n;
}

ssize_t Readn(int fd, void *vptr, size_t n)
{
    ssize_t nread;
    ssize_t nleft = n;
    char* ptr = (char*)vptr;
    while(nleft > 0)
    {
        nread = read(fd,ptr,nleft);
        if(nread < 0)
        {
            if(errno == EINTR)
                nread = 0;
            else
                return -1;
        }
        else if(nread == 0)
            break;

        nleft -= nread;
        ptr += nread;
    }
    return n-nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
    ssize_t nwrite;
    ssize_t nleft = n;
    char* ptr = (char*)vptr;
    while(nleft > 0)
    {
        nwrite = write(fd,ptr,nleft);
        if(nwrite < 0)
        {
            if(errno == EINTR)
                nwrite = 0;
            else
                return -1;
        }
        else if(nwrite == 0)
            break;

        nleft -= nwrite;
        ptr += nwrite;
    }
    return n;
}

static ssize_t my_read(int fd, char *ptr)
{
    static int read_cnt;
    static char* read_ptr;
    static char read_buf[100];

    if(read_cnt <= 0)
    {
again:
        read_cnt = read(fd,read_buf,sizeof(read_buf));
        if(read_cnt == -1)
        {
            if(errno == EINTR)
                goto again;
            else
                return -1;
        }
        else if(read_cnt == 0)
            return 0;

        read_ptr = read_buf;
    }
    --read_cnt;
    *ptr = *read_ptr++;

    return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
    ssize_t n,rc;
    char c,*ptr;
    ptr = (char*)vptr;

    for(n=1 ;n<maxlen ;++n)
    {
        rc = my_read(fd,&c);
        if(rc == 1)
        {
            *ptr++ = c;
            if(c == '\n')
                break;
        }
        else if(rc == 0)
        {
            *ptr = 0;
            return n-1;
        }
        else if(rc == -1)
            return -1;
    }
    *ptr = 0;
    return n;
}

5. 高并发服务器(多进程)

1. 程序流程

  1. Socket函数创建监听套接字

  2. Bind函数绑定IP地址和端口号

  3. Listen函数设置最大监听数

  4. while(1){

  5. Accept函数阻塞等待客户端接入,创建通信套接字

  6. fork创建子进程

    子进程:

    1. Close关闭监听套接字文件描述符
    2. Read函数接收客户端发送的数据(当Read的返回值为0的时候,Close通信套接字文件描述符,子进程退出)
    3. 小写转大写
    4. Write函数将处理后的数据发送给客户端
    5. Close关闭通信套接字文件描述符

    父进程:

    1. 关闭通信套接字文件描述
    2. 注册 SIGCHLD 的信号捕捉函数,用来回收子进程
    3. continue 继续回到循环,等待新的客户端接入
  7. }

2. 代码实现

#include <ctype.h>
#include "wrap.h"
#include <sys/wait.h>
#include <signal.h>

#define SRV_PORT 9999

void func(int signo)
{
    while(1)
    {
        int ret = waitpid(-1,NULL,0);
        if(ret == -1)
            break;
    }
    return ;
}

int main()
{
    int lfd,cfd;
    socklen_t clt_addr_len;
    pid_t pid;
    char clt_IP[1024];
    char buf[BUFSIZ] = {0};

    struct sockaddr_in srv_addr,clt_addr;
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(SRV_PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);


    lfd = Socket(AF_INET,SOCK_STREAM,0);

    Bind(lfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));

    Listen(lfd,128);

    char SRV_IP[1024];
    printf("服务器已开启ip:%s,port:%d\n",
           inet_ntop(AF_INET,&srv_addr.sin_addr.s_addr,SRV_IP,sizeof(SRV_IP)),
           ntohs(srv_addr.sin_port));

    while(1)
    {
        clt_addr_len = sizeof(clt_addr);
        cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);

        printf("客户端ip:%s,port:%d接入服务器\n",
               inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,clt_IP,sizeof(clt_IP)),
               ntohs(clt_addr.sin_port));

        pid = fork();
        if(-1 == pid)
        {
            sys_err("fork error");
        }
        else if(0 == pid)
        {
            Close(lfd);
            break;
        }
        else
        {
            Close(cfd);

            struct sigaction act = {
                .sa_handler = func
            };
            int ret = sigaction(SIGCHLD,&act,NULL);
            if(-1 == ret)
            {
                sys_err("sigaction error");
            }
            
            continue;
        }
    }

    if(0 == pid)
    {
        while(1)
        {
            int ret = Read(cfd,buf,sizeof(buf));
            if(0 == ret)
            {
                printf("客户端ip:%s,port:%d断开连接\n",
                       clt_IP,ntohs(clt_addr.sin_port));
                break;
            }
            for(int i=0 ;i<ret ;++i)
            {
                buf[i] = toupper(buf[i]);
            }
            Write(cfd,buf,ret);
            printf("发给ip:%s,port:%d数据:%s",clt_IP,ntohs(clt_addr.sin_port),buf);
        }
        Close(cfd);
    }
    return 0;
}

注意:accept函数的参数中传入传出参数是客户端的IP地址变量

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值