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