前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2-UNIX网络编程-进阶学习前的基础知识储备

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

作者头像
zoujunjie202
发布2022-07-27 15:41:49
3890
发布2022-07-27 15:41:49
举报
文章被收录于专栏:zoujunjie202

诚实点,并不是假设读的人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、读写状态

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 zoujunjie202 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档