套接字错误处理函数的封装思想及函数实现

一、套接字错误处理函数的封装思想

在上篇文章中的 CS 模型,存在 bug:
先关 server,再关 client,立即再启动 server,此时会发现无法启动。原因是:先关 server,再关 client,执行 shell 命令netstat -apn | grep 端口号,会发现服务器处于
“TIME_WAIT”状态,即不是真正的退出状态,也就
意味着,原先 server 端口号并没有被释放掉,再次启动 server,因为端口号是固定的,而且正在被占用中,所以无法启动,问题出现在 bind 函数调用失败。
解决方法就是端口复用,代码为了突出逻辑性,并没有进行错误提示,这就导致使用者不知道错误的点,但如果进行错误提示,又会使代码的可读性变差,因此,采用错误处理函数封装。
错误处理函数封装本质是对库函数进行从新封装:
(1)完全按照库函数的原型重新封装函数,注意:函数名是原函数名首字母大写 。
(2)封装的函数体的内部实现添加错误处理,这样做的好处是使用时候跟原库函数没有区别(因为是按库函数进行封装的,调用时只需注意函数名即可);相较于使用原库函数,不需要自己进行错误判断,因为内部已经封装好了;利用 man 帮助文档时候,函数名不区分大小写,例如对于 man 命令,Socket 和 socket 是一样的,因此在 man_page 中可以直接查看封装后函数的使用方法。
使用方法:
对 server.c 和 client.c 中使用的函数,重新封装,一并放入 wrap.c 中(此外会有一个 wrap.h,对这些封装函数做出声明),通过联合编译实现对原库函数的功能扩展。
以server.c 为例:

gcc server.c wrap.c -o server

二、套接字错误处理函数的实现

wrap.h:

#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
int tcp4bind(short port,const char *IP);
#endif

wrap.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
void perr_exit(const char *s) // 错误信息处理函数 // 这层封装的目的是为了减少下方代码量
{
  perror(s); // 输出错误信息
  exit(-1); // 退出进程
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) // 阻塞等待客户端连接函数
{
  int n; // 控制返回值
  again:
  if ((n = accept(fd, sa, salenptr)) < 0)
  {
    if ((errno == ECONNABORTED) || (errno == EINTR))
      goto again; // 如果依据错误号,表示错误是“连接时异常断 开”或者“阻塞时被信号打断”
    else (这2个错误都会使accept调用失败),需要重新调用
      perr_exit("accept error");
  }
  return n; 
}

int Bind(int fd, const struct sockaddr *sa, socklen_t salen) // 绑定端口和IP
{
  int n; // 返回值,与客户端连接的套接字
  if ((n = bind(fd, sa, salen)) < 0)
    perr_exit("bind error");
  return n; 
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen) // 与服务器建立连接
{
  int n; // 返回值,与服务器连接的套接字
  if ((n = connect(fd, sa, salen)) < 0)
    perr_exit("connect error");
  return n;
}

int Listen(int fd, int backlog) // 监听同时建立连接的上限数
{
  int n;
  if ((n = listen(fd, backlog)) < 0)
    perr_exit("listen error");
  return n; 
}

int Socket(int family, int type, int protocol) // 创建套接字
{
  int n;
  if ((n = socket(family, type, protocol)) < 0)
    perr_exit("socket error");
  return n; 
}

ssize_t Read(int fd, void *ptr, size_t nbytes) 
{
  ssize_t n;
  again:
  if ( (n = read(fd, ptr, nbytes)) == -1) // > 0,读取的字节数, = 0,读到末尾(对端关闭),= -1 读异常
  {
    if (errno == EINTR) // 如果读阻塞时候被信号打断,恢复原读阻塞
      goto again;
    else
      return -1;
  }
  return n; 
}

ssize_t Write(int fd, const void *ptr, size_t nbytes) 
{
  ssize_t n;
  again:
  if ( (n = write(fd, ptr, nbytes)) == -1)
  {
    if (errno == EINTR) // 如果写阻塞时候被信号打断,恢复原写阻塞
      goto again;
    else
      return -1; 
  }
  return n;
}

int Close(int fd) // 关闭套接字
{
  int n;
  if ((n = close(fd)) == -1)
    perr_exit("close error");
  return n; 
}

// 参数:文件描述符,存放读取数据的缓冲区的首地址(传出参数),要读多少字节(小于vptr长度)
// 返回值:成功 返回已读取的字节数 ;失败 :返回-1
ssize_t Readn(int fd, void *vptr, size_t n) // 功能:读取n个字节 
{ 
  size_t nleft; // usigned int 剩余未读取的字节数
  ssize_t nread; // int 实际读到的字节数
  char *ptr; // 指针,用于定位开始读的位置
  ptr = vptr; // 从缓存区头部开始存
  nleft = n; // 指定要读取的字节数
  while (nleft > 0) // 如果还有字节没有被读  一直读取
  { 
    if ((nread = read(fd, ptr, nleft)) < 0) // 如果发现读失败
    { 
      if (errno == EINTR)
        nread = 0; // 如果是因为信号打断造成的,认为没读到,但不退出(直接跳转到3)
      else
        return -1; // 如果不是信号打断的,认为读异常,退出,返回-1
    } 
    else if (nread == 0) // 如果发现全部读完(或对方关闭),退出循环,不再继续读
    break; // 从此处退出循环,意味着函数调用成功,返回n
 //  读行为正常
    nleft -= nread; // 更新剩余未读取的字节数
    ptr += nread; // 指针后移,定位在下次开始存储的位置
  }
  return n - nleft; // 返回已读取的字节数
}

ssize_t Writen(int fd, const void *vptr, size_t n) // 功能:写入n个字节
{
  size_t nleft;
  ssize_t nwritten;
  const char *ptr;
  ptr = vptr;
  nleft = n;
  while (nleft > 0) 
  {
    if ( (nwritten = write(fd, ptr, nleft)) <= 0) 
    {
      if (nwritten < 0 && errno == EINTR)
        nwritten = 0; // 本质:忽略 信号打断造成的情况
      else
        return -1;
    }
    nleft -= nwritten;
    ptr += nwritten; 
  }
  return n; 
}

static ssize_t my_read(int fd, char *ptr) // Readline() 的子函数
{
  static int read_cnt; // read函数返回值,代表读到多少个字节
  static char *read_ptr;
  static char read_buf[100]; // 存放读取内容的缓存区
  if (read_cnt <= 0) 
  {
    again:
    if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) // 1. 正常读,但是如果读失败
    {
       if (errno == EINTR)
         goto again;
         return -1; // 读取1异常 返回-1 }
       else if (read_cnt == 0) // 2. 正常读到结尾
         return 0; // 读取到末尾(对端关闭) 返回0
       read_ptr = read_buf; // 3. 既不是异常,也没读到结尾,read_ptr 指向 存储缓存区的首地址
    }
    read_cnt--; 
    *ptr = *read_ptr++;
    return 1; // 读取成功 返回1 
}

// 参数:文件描述符,存放读取数据缓冲区的首地址,缓冲区的大小 // 返回值:成功 0; 失败 -1
ssize_t Readline(int fd, void *vptr, size_t maxlen) // 功能:读取一行
{
  ssize_t n, rc; // my_read 函数返回值
  char c, *ptr; // 定义my_read读取的字符 // 定义开始存储的位置
  ptr = vptr; // 最开始从缓存区头开始存储
  for (n = 1; n < maxlen; n++)
  {
    if ( (rc = my_read(fd, &c)) == 1) // 如果读取1字节成功
    { 
       *ptr++ = c; // 将读取的结果,赋给ptr,同时ptr后移
      if (c == '\n')
        break; // 如果到达行末尾,退出if,意味着1行读取成功,补0,然后return
    }
    else if (rc == 0) 
    { 
      *ptr = 0; // 如果正常读到结尾,直接结尾补0
      return n - 1;
    }
    else
      return -1; // 剩余情况就是读异常,返回-1 }*ptr = 0; // 最后位置补0
  return n; 
}

int tcp4bind(short port,const char *IP) 
{
  struct sockaddr_in serv_addr;
  int lfd = Socket(AF_INET,SOCK_STREAM,0);
  bzero(&serv_addr,sizeof(serv_addr));
  if(IP == NULL) 
  {
 //如果这样使用 0.0.0.0,任意 ip 将可以连接
   serv_addr.sin_addr.s_addr = INADDR_ANY; 
  }
 else
 {
   if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0)
  {
     perror(IP); // 转换失败
     exit(1);
   }
 }
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(port);
  Bind(lfd, (struct sockaddr *)&serv_addr,sizeof(serv_addr));
  return lfd; 
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值