首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >2-UNIX网络编程-进阶学习前的基础知识储备

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

作者头像
zoujunjie202
发布于 2022-07-27 07:41:49
发布于 2022-07-27 07:41:49
44800
代码可运行
举报
文章被收录于专栏:zoujunjie202zoujunjie202
运行总次数:0
代码可运行

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

- C语言错误处理

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#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
代码运行次数:0
运行
AI代码解释
复制
//
//  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
代码运行次数:0
运行
AI代码解释
复制
#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
代码运行次数:0
运行
AI代码解释
复制
//
//  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
代码运行次数:0
运行
AI代码解释
复制
//
//  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
代码运行次数:0
运行
AI代码解释
复制
#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
代码运行次数:0
运行
AI代码解释
复制
#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
代码运行次数:0
运行
AI代码解释
复制
 编译脚本
 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
代码运行次数:0
运行
AI代码解释
复制
#文件名是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
代码运行次数:0
运行
AI代码解释
复制

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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
1-UNIX网络编程-Socket套接字编程简介
触发学习UNIX网络编程的动力在于前段时间需要开发一个接入服务,需要考虑比较高的并发处理能力,且尽量少占用的机器资源,选用了JAVA的Netty框架,学习过程产生不少疑问,限于基础知识太薄弱无法理解原理,所以开始关注UNIX编程。
zoujunjie202
2022/07/27
1.3K0
1-UNIX网络编程-Socket套接字编程简介
简单的客户机服务器投射模拟
下面模拟了,简单的客户机服务器投射模拟的过程。客户机像服务器发送数据,服务器接受到数据后,发送回给客户机。再由客户机打印出来。 需要的函数: 网络方面 服务器 socket(AF_INET,SOCK_STREAM,0);  AF_INET表示IPV4,SOCK_STREAM表示基于字节流的,0表示协议由前面两个参数组合而成。返回描述符 bind(sockdf,(struct sockaddr*)servaddr,sizeof(servaddr));   用于把描述符与本地协议地址联系起来。 listen
用户1154259
2018/01/17
1K0
UNIX(进程间通信):17 深入理解unix域
unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法。
用户3479834
2021/03/04
1.9K0
UNIX(进程间通信):17 深入理解unix域
205-ESP32_SDK开发-TCP服务器(select方式,支持多连接,高速高并发传输)
https://www.cnblogs.com/orlion/p/6119812.html
杨奉武
2021/12/12
1.2K0
205-ESP32_SDK开发-TCP服务器(select方式,支持多连接,高速高并发传输)
UDP套接口编程
常用的UDP实现的程序:DNS域名系统,NFS网络文件系统,SNMP简单网络管理协议 ssize_t recvfrom(int sockfd,void *buff,size_t nbytes,int flags,struct sockaddr * from,socklen_t *addrlen); ssize_t sendto(int sockfd,void *buff,size_t nbytes,int flags,struct sockaddr * to,socklen_t addrlen); sock
用户1154259
2018/01/17
1K0
epoll,求知者离我近点
上网一搜epoll,基本是这样的结果出来:《多路转接I/O – epoll模型》,万变不离这个标题。 但是呢,不变的事物,我们就更应该抓出其中的重点了。 多路、转接、I/O、模型。 别急,先记住这几个词,我比较喜欢你们看我文章的时候带着问题。
看、未来
2020/08/25
5520
epoll,求知者离我近点
select()函数详解
http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html
bear_fish
2018/09/20
1.9K0
select()函数详解
UNIX网络编程卷1(第三版) 客户/服务器程序示例
创建TCP套接字。在待绑定到该套接字的网际网套接字地址结构中填入通配地址(INADDR_ANY)和服务器众所周知的端口(SERV_PORT).捆绑通配地址是告诉系统:要是系统是多宿主机,我们将接受目的地址为任何本地接口的连接。listen把该套接字转成一个监听套接字。
心跳包
2020/08/31
4670
I/O复用——单进程服务器(select版)
为了可以处理多个客户的请求,我们之前一直使用多进程TCP并发服务器,socket()监听一个套接口,accept()多个用户,父进程监听listenfd,子线程们在connfd上进行应答处理。
jackieluo
2019/01/06
2.1K0
I/O复用——单进程服务器(select版)
linux 网络编程 I/O复用 select,poll ,epoll
http://blog.csdn.net/zs634134578/article/details/19929449
bear_fish
2018/09/20
2.8K0
【LINUX/UNIX网络编程】之简单多线程服务器(多人群聊系统)
RT,Linux下使用c实现的多线程服务器。这个真是简单的不能再简单的了,有写的不好的地方,还希望大神轻拍。(>﹏<)
马三小伙儿
2018/09/12
1.2K0
【LINUX/UNIX网络编程】之简单多线程服务器(多人群聊系统)
UNIX网络编程卷1(第三版)基本TCP套接字编程
其中family参数指明协议族,type参数指明套接字类型,proctocol参数为协议类型或者0
心跳包
2020/08/31
6450
网络编程-一个简单的echo程序(0)
在上一篇《网络编程-从TCP连接的建立说起》中简单介绍了TCP连接的建立,本文暂时先抛开TCP更加详细的介绍,来看看如何实现一个简单的网络程序。
编程珠玑
2019/07/12
5990
网络编程-一个简单的echo程序(0)
并发服务器代码实现(多进程/多线程)
当涉及到构建高性能的服务器应用程序时,我们通常会考虑使用并发服务器来处理多个客户端请求。在并发服务器中,多进程和多线程是两种常见的并发模型,它们都有各自的优点和适用场景。本文将介绍多进程和多线程并发服务器的基础知识。
mindtechnist
2025/05/09
1080
并发服务器代码实现(多进程/多线程)
I/O多路复用select/poll/epoll
早期操作系统通常将进程中可创建的线程数限制在一个较低的阈值,大约几百个。因此, 操作系统会提供一些高效的方法来实现多路IO,例如Unix的select和poll。现代操作系统中,线程数已经得到了极大的提升,如NPTL线程软件包可支持数十万的线程。
WindSun
2019/09/09
1.4K0
I/O多路复用select/poll/epoll
SOCKET网络编程 (通俗易懂入门篇)
struct sockaddr :很多网络编程函数的出现早于IPV4协议,为了向前兼容,现在sockaddr都退化成(void *)结构了。 传递一个地址给函数,然后由函数内部再强制类型转换为所需的地址类型。
看、未来
2020/08/25
1.2K0
SOCKET网络编程 (通俗易懂入门篇)
UNIX域协议(命名套接字)
这里主要介绍命名UNIX域套接字 1.什么是UNIX域套接字 Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务通信的一种方式。是进程间通信(IPC)的一种方式。 它提供了两类套接字:字节流套接字(有点像TCP)和数据报套接字(有点像UDP) UNIX域数据报服务是可靠的,不会丢失消息,也不会传递出错。 IP协议标识客户服务器是通过IP地址和端口号实现的,UNIX域协议中用于标识客户机和服务器的协议地址的是普通文件系统中的路径名。 2.UNIX域协议特点 1)UNIX域套接字域TCP套
xcywt
2018/01/12
3.3K1
网络编程学习笔记7-TCP使用的注意事项(附代码)
根据TCP 协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了
opencode
2022/12/26
3640
网络编程学习笔记7-TCP使用的注意事项(附代码)
网络编程入门_回显服务器
sockaddr和sockaddr_in包含的数据都是一样的,但他们在使用上有区别: 程序员不应操作sockaddr,sockaddr是给操作系统用的 程序员应使用sockaddr_in来表示地址,sockaddr_in区分了地址和端口,使用更方便。 一般的用法为: 程序员把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数
yifei_
2022/11/14
7530
网络编程入门_回显服务器
网络编程API-下 (I/O复用函数)[通俗易懂]
IO复用是Linux中的IO模型之中的一个,IO复用就是进程预先告诉内核须要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理。从而不会在单个IO上堵塞了。
全栈程序员站长
2022/07/08
4440
网络编程API-下 (I/O复用函数)[通俗易懂]
相关推荐
1-UNIX网络编程-Socket套接字编程简介
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档