2-UNIX网络编程-进阶学习前的基础知识储备

2022-07-27 15:41:49 浏览数 (1)

诚实点,并不是假设读的人C语言基础很差,而事实是,自己的C语言基础实在太差,要补学基础知识之后才可以进行后续的学习,惭愧!

- C语言错误处理

C 语言不提供对错误处理的直接支持,而是以返回值的形式来表示错误。发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个全局变量错误代码 errno,表示在函数调用期间发生了错误。errno.h 头文件中找到各种各样的错误代码,如下截图展示其中一小部分错误码及对应含义。

程序可以通过检查返回值决定采取哪种错误处理的动作。以下以封装socket函数为例,描述一个错误处理的常见手段。代码基于上一个章节的sever端代码来扩展:

代码语言:javascript复制
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// 错误处理有关的头文件
#include <errno.h>
#include <stdarg.h>        /* ANSI C header file */
#include <syslog.h>        /* for syslog() */

#define MAXLINE     4096
#define LISTENQ     1024

int daemon_proc; // 一个全局变量,用于控制应用日志是否输出到系统日志,本案例不使用系统日志,因此变量会保持为常量0.
// 4、打印错误日志
static void err_doit(int errnoflag, int level, const char *fmt, va_list ap){
    int     errno_save;
    long    n;
    char    buf[MAXLINE   1];
    
    errno_save = errno; // 5、记录全局错误码
#ifdef    HAVE_VSNPRINTF
    vsnprintf(buf, MAXLINE, fmt, ap);/* safe */
#else
    vsprintf(buf, fmt, ap);/* not safe 按格式打印日志 */ // 本案例没有定义 HAVE_VSNPRINTF ,所以直接走这个函数
#endif
    n = strlen(buf);
    // 6、如果错误码存在,则获取错误码对应的描述
    if (errnoflag)
        snprintf(buf   n, MAXLINE - n, ": %s", strerror(errno_save));
    strcat(buf, "n");
    
    if (daemon_proc) {
        syslog(level, buf);//打印日志到系统日志,系统日志默认是/var/log/messages,具体知识点使用的时候再翻资料
    } else {
        fflush(stdout);        /* in case stdout and stderr are the same */
        fputs(buf, stderr);
        fflush(stderr);
    }
    return;
}

//3、系统调用发生致命错误时,往控制台打印日志并退出系统。【Tips】fmt参数可以是一个带格式的字符串
void err_sys(const char *fmt, ...){
    va_list ap;
    va_start(ap, fmt); // 用省略号指定参数列表时,用va_start函数来获取参数列表中的参数,使用完毕后调用va_end()结束
    err_doit(1, LOG_ERR, fmt, ap);
    va_end(ap);
    exit(1); // 正常退出的代码是 0
}

// 2、socket包裹函数,命名为Socket,方便区分使用
int Socket(int family, int type, int protocol){
    int n;
    if ( (n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return(n);
}

int main(int argc , char **argv ){
    int                 listenfd,connfd;
    ssize_t             n;
    char                buff[MAXLINE 1];
    struct sockaddr_in  servaddr;
    // socket
    listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 1、替换为包裹函数
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(13);
    // bind
    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    // listen
    listen(listenfd,LISTENQ);
    for(;;){
        connfd = accept(listenfd,NULL,NULL);
        while( ( n = read(connfd,buff,MAXLINE)) > 0 ){
            printf("receive message from client. message = %s",buff);
            write(connfd,buff,strlen(buff));
            bzero(&buff,sizeof(buff));
        }
        close(connfd);
    }
}

- 项目文件组织

添加Socket包裹函数之后,单个文件的代码变多,而且代码的职责的各异,在进一步添加代码之前,把代码目录组织好调理会更清晰。如下截图,抽离了一个头文件global.h,预留一个main.c作为程序主入口,把包裹函数和error处理函数放到lib目录,server和client保留在app目录。

再贴一次全部的代码:

--global.h

代码语言:javascript复制
//
//  global.h
//
//ifndef 这段代码的作用是方式重复引入global.h文件
#ifndef global_h
#define global_h

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

// 错误处理有关的头文件
#include <errno.h>
#include <stdarg.h>        /* ANSI C header file */
#include <syslog.h>        /* for syslog() */

#define MAXLINE     4096
#define LISTENQ     1024

// socket 包裹函数
int     Socket(int, int, int);

// ** error 错误处理函数列表
void    err_sys(const char *, ...);

// app
void    start_client(void);
void    start_server(void);

#endif /* global_h */

--main.c

代码语言:javascript复制
#include "global.h"

int main(int argc, const char * argv[]){
    if(strcmp(argv[1],"start_server") == 0){
        start_server();
    }
    if(strcmp(argv[1],"start_client") == 0){
        start_client();
    }
}

--lib/wraper.c

代码语言:javascript复制
//
//  wraper.c
//  c
//
//  Created by Junjie Zou on 2019/7/31.
//  Copyright © 2019 Junjie Zou. All rights reserved.
//

#include "global.h"

// 2、socket包裹函数,命名为Socket,方便区分使用
int Socket(int family, int type, int protocol){
    int n;
    if ( (n = socket(family, type, protocol)) < 0)
        err_sys("socket error");
    return(n);
}

--lib/error.c

代码语言:javascript复制
//
//  error.c
//  c
//
//  Created by Junjie Zou on 2019/7/31.
//  Copyright © 2019 Junjie Zou. All rights reserved.
//

#include "global.h"

int daemon_proc; // 一个全局变量,用于控制应用日志是否输出到系统日志,本案例不使用系统日志,因此变量会保持为常量0.
// 4、打印错误日志
static void err_doit(int errnoflag, int level, const char *fmt, va_list ap){
    int     errno_save;
    long    n;
    char    buf[MAXLINE   1];
    
    errno_save = errno; // 5、记录全局错误码
#ifdef    HAVE_VSNPRINTF
    vsnprintf(buf, MAXLINE, fmt, ap);/* safe */
#else
    vsprintf(buf, fmt, ap);/* not safe 按格式打印日志 */ // 本案例没有定义 HAVE_VSNPRINTF ,所以直接走这个函数
#endif
    n = strlen(buf);
    // 6、如果错误码存在,则获取错误码对应的描述
    if (errnoflag)
        snprintf(buf   n, MAXLINE - n, ": %s", strerror(errno_save));
    strcat(buf, "n");
    
    if (daemon_proc) {
        syslog(level, buf);//打印日志到系统日志,系统日志默认是/var/log/messages,具体知识点使用的时候再翻资料
    } else {
        fflush(stdout);        /* in case stdout and stderr are the same */
        fputs(buf, stderr);
        fflush(stderr);
    }
    return;
}

//3、系统调用发生致命错误时,往控制台打印日志并退出系统。【Tips】fmt参数可以是一个带格式的字符串
void err_sys(const char *fmt, ...){
    va_list ap;
    va_start(ap, fmt); // 用省略号指定参数列表时,用va_start函数来获取参数列表中的参数,使用完毕后调用va_end()结束
    err_doit(1, LOG_ERR, fmt, ap);
    va_end(ap);
    exit(1); // 正常退出的代码是 0
}

--app/client.c

代码语言:javascript复制
#include "global.h"

void start_client(){
    int                 sockfd;
    ssize_t             n;
    char                sendline[MAXLINE 1] ,recvline[MAXLINE 1];
    struct sockaddr_in  servaddr;
    // socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(13);
    inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr);
    // connect
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    
    while( fgets(sendline,MAXLINE,stdin) != NULL ){
        write(sockfd,sendline,strlen(sendline));
        bzero(&recvline,sizeof(recvline));
        if( (n = read(sockfd,recvline,MAXLINE) ) > 0 ){
            recvline[n] = 0 ;
            printf("receive message from server. message = %s",recvline);
            // fputs(recvline,stdout);
        }
        bzero(&sendline,sizeof(sendline));
    }
    close(sockfd);
    exit(0);
}

--app/server.c

代码语言:javascript复制
#include "global.h"

void start_server(){
    int                 listenfd,connfd;
    ssize_t             n;
    char                buff[MAXLINE 1];
    struct sockaddr_in  servaddr;
    // socket
    listenfd = Socket(AF_INET, SOCK_STREAM, 0); // 1、替换为包裹函数
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(13);
    // bind
    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    // listen
    listen(listenfd,LISTENQ);
    for(;;){
        connfd = accept(listenfd,NULL,NULL);
        while( ( n = read(connfd,buff,MAXLINE)) > 0 ){
            printf("receive message from client. message = %s",buff);
            write(connfd,buff,strlen(buff));
            bzero(&buff,sizeof(buff));
        }
        close(connfd);
    }
}

代码准备好之后,编译、链接得到可以执行文件

代码语言:javascript复制
 编译脚本
 gcc -I ./ -c app/server.c -o app/server.o
 gcc -I ./ -c app/client.c -o app/client.o
 gcc -I ./ -c lib/wraper.c -o lib/wraper.o
 gcc -I ./ -c lib/error.c -o lib/error.o
 gcc -I ./ -c main.c -o main.o
 gcc -o main app/server.o app/client.o lib/wraper.o lib/error.o main.o

示例

1)错误提示

要出现上面这种报错很简单,如下,修改以下Socket的协议类型即可。

2)正常输出

- 编写Makefile

关注回上面的编译脚本,首先需要逐个文件进行编译,然后链接所有的文件,项目文件很多的情况下编译工作将会非常繁琐,所以需要引入Makefile来协助编译、链接生成可执行文件。

编写第一个版本的makefile,结合上面执行过的编译脚本,这个makefile文件久比较好理解了。

代码语言:javascript复制
#文件名是makefile
#生成main,右边为目标,左边是所依赖项。(#gcc之前需要用Tab,不是空格,在vc中编辑的话不好输入,直接使用vi makefile命令进行编辑更方便)
main:lib/wraper.o lib/error.o app/server.o app/client.o main.o
  gcc -o main lib/wraper.o lib/error.o app/server.o app/client.o main.o
wraper.o:lib/wraper.c  global.h
  gcc -c lib/wraper.c -o lib/wraper.o -I./
error.o:lib/error.c  global.h
  gcc -c lib/error.c -o lib/error.o -I./
server.o:app/server.c  global.h
  gcc -c app/server.c -o app/server.o -I./
client.o:app/client.c  global.h
  gcc -c app/client.c -o app/client.o -I./
main.o:main.c global.h
  gcc -c main.c -o main.o -I./
#清理命令
clean:
  rm -f main.o lib/*.o app/*.o

本想找一个通用版的makefile文件的,但是发现有几项语法不大清晰,所以现在使用这个简陋版本先,留一个学习任务后面再补充。

- 完成server和client中其他函数的包裹

代码语言:javascript复制

int Bind(int listenfd, const struct sockaddr *servaddr, socklen_t socklen){
    int n;
    if( (n = bind(listenfd, servaddr, socklen)) < 0 )
        err_sys("bind error");
    return (n);
}

int Listen(int listenfd, int length){
    int n ;
    if( (n = listen(listenfd , length ) ) < 0 )
        err_sys("Listen error");
    return (n);
}

int Accept(int listenfd, struct sockaddr * addr, socklen_t * length){
    int n ;
    if( (n = accept(listenfd,addr,length )) < 0 )
        err_sys("accept error");
    return (n);
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen){
    int n;
    if ( (n = connect(fd, sa, salen)) < 0)
        err_sys("connect error");
    return (n);
}

到现在为止,基础知识就准备到这里,花了不少时间,基础知识太薄弱,效率也不高。

下周继续进行网络编程的知识点:

1、改造为并发服务器

2、读写状态

0 人点赞