UNIX网络编程读书笔记(一)第一章 简介

守护程序(daemon):一个长时间运行的程序。

包裹函数定义
包裹函数其实就是封装函数,调用一个函数来实现这个功能,但是我们通常不在这个函数里面来定义它,只是调用,把一个函数做好封装后,以后到哪里都可以用这个函数,只要知道这个函数派什么用处,理解接口就可以了,不需要知道函数是怎么做的。其实是也可以有上锁机制在里面,具有排他性,不让别人来修改它。

POSIX
表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX), POSIX标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称,其正式称呼为IEEE1003,而国际标准名称为ISO/IEC 9945。

如下程序是TCP当前时间查询客户程序的一个实现,该客户与其服务器建立一个TCP连接后,服务器以直观可读格式简单地送回当前时间和日期。

/*
	intro/daytimetcpcli.c
	时间获取客户程序
*/
#include <stdio.h>  
#include <sys/socket.h>//socket使用
#include <sys/types.h> //基本系统数据类型:size_t,time_t,pid_t等类型
#include <netinet/in.h>  //协议族
#include <string.h>
#include <stdlib.h>
#include "my_err.h"
#define	MAXLINE		4096	/* max text line length */
int main(int argc, char **argv)
{
	int sockfd , n ;
	char recvline[MAXLINE+1];
	struct sockaddr_in servaddr;
	if(argc != 2)
		err_quit("usage:a.out <IPaddress>");
	if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
		err_sys("socket error");
	bzero(&servaddr , sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5000);    /*daytime server*/
	if(inet_pton(AF_INET,argv[1],&servaddr.sin_addr) <= 0)/*将IP地址在“点分十进制”和“二进制整数”之间转换*/
		err_quit("inet_pton error for %s", argv[1]);
	if(connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr))<0)
		err_sys("connect error");
	while((n = read(sockfd,recvline,MAXLINE)) > 0){
		recvline[n] = 0;   /*null terminate*/
		if(fputs(recvline,stdout) == EOF)
			err_sys("fpute error");
	}
	if (n < 0)
		err_sys("read error");
	exit(0);
}
  • 创建TCP套接字:socket函数创建一个网际(AF_INET)字节流(SOCK_STREAM)套接字,该函数返回一个小整数描述符,以后所有的函数调用都用该描述符来标识这个套接字。
  • 指定服务器的IP地址和端口:服务器的IP地址端口号填入一个网际套接字地址结构(一个名为servaddr的sockaddr_in结构变量)。网际套接字地址结构中IP地址和端口号这两个成员必须使用特定格式,htons(“主机到网络短整数”)去转换二进制端口号,inet_pton(“呈现形式到数值”)把ASCII命令行参数转换为合适的格式。
  • 建立和服务器的链接:connect函数应用于一个TCP套接字时,将与由它的第二个参数指向的套接字地址结构指定的服务器建立一个TCP连接。该套接字结构的长度也必须作为该函数的第三个参数指定,对于网际套接字地址结构,用sizeof操作符指定。每当一个套接字函数需要一个指向某个套接字地址结构的指针时,这个指针必须强制类型转换成一个指向通用套接字地址结构的指针
  • 读入并输出服务器的应答:通常服务器返回包含所有字节的单个分节,但是如果数据量很大,就不能保证一次read调用能返回服务器的整个应答。因此从TCP套接字读取数据时,总是需要把read编写在某个循环中,当read返回0(表明对端关闭连接)或者负值(表明发生错误)时终止循环。
  • 包裹函数(wrapper function):每个包裹函数完成实际的函数调用,检查返回值,并在发生错误时终止进程。 描述一个网络中各个协议层的常用方法是使用国际标准化组织(International Organization for Standardization,ISO)的计算机通信开发系统互连(open systems interconnection,OSI)模型。

一个简单的时间获取服务器程序

#include<stdio.h>  
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include "my_err.h"
#include	<time.h>
#define	MAXLINE		4096	/* max text line length */
#define	LISTENQ		1024	/* 2nd argument to listen() */
int Socket(int family, int type, int protocol)
{
	int		n;

	if ( (n = socket(family, type, protocol)) < 0)
		err_sys("socket error");
	return(n);
}
void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
	if (bind(fd, sa, salen) < 0)
		err_sys("bind error");
}
void Listen(int fd, int backlog)
{
	char	*ptr;

		/*4can override 2nd argument with environment variable */
	if ( (ptr = getenv("LISTENQ")) != NULL)
		backlog = atoi(ptr);

	if (listen(fd, backlog) < 0)
		err_sys("listen error");
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
	int		n;

again:
	if ( (n = accept(fd, sa, salenptr)) < 0) {
#ifdef	EPROTO
		if (errno == EPROTO || errno == ECONNABORTED)
#else
		if (errno == ECONNABORTED)
#endif
			goto again;
		else
			err_sys("accept error");
	}
	return(n);
}
void Write(int fd, void *ptr, size_t nbytes)
{
	if (write(fd, ptr, nbytes) != nbytes)
		err_sys("write error");
}
void Close(int fd)
{
	if (close(fd) == -1)
		err_sys("close error");
}
int main(int argc, char **argv)
{
	int					listenfd, connfd;
    socklen_t         len;
	struct sockaddr_in	servaddr,cliaddr;
	char				buff[MAXLINE];
	time_t				ticks;

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   /*要是服务器有多个网络接口,服务器就可以在任意网络接口上接收客户连接*/
	servaddr.sin_port        = htons(5000);	/* daytime server */

	Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);/*把套接字转换成一个监听套接字*/

	for ( ; ; ) {
        len = sizeof(cliaddr);
		connfd = Accept(listenfd, (struct sockaddr *) &cliaddr, &len);
        printf("connection from %s port %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr,buff,sizeof(buff)),ntohs(cliaddr.sin_port));
        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buff, strlen(buff));

		Close(connfd);
	}
}
  • 监听套接字:listen函数将套接字转换成一个监听套接字,这样来自客户的外来连接就可在该套接字上由内核接受。LISTENQ指定系统内核允许在这个监听描述符上排队的最大客户连接数。
  • 接受客户连接,发送应答:通常情况,服务器进程在accept调用中被投入睡眠,等待某个客户连接的到达并被内核接受。TCP连接使用三路握手来建立连接,握手完毕accept返回一个已连接描述符,用于与客户通信。
  • 终止连接:服务器通过close关闭与客户的连接。该调用引发正常的TCP连接终止序列:每个方向上发送一个FIN,每个FIN又由各自对端确认。

在这里插入图片描述

  1. OSI模型的底下两层是随系统提供的设备驱动程序和网络硬件。
  2. 网络层由IPv4和IPv6两个协议处理。网络层由IPv4和IPv6两个协议处理。
  3. 传输层有TCP或UDP,它们之间的间隙表明网络应用绕过传输层直接使用IPv4或IPv6是可能的,这就是所谓的原始套接字(raw socket)。
  4. OSI模型的顶上三层被合并成一层,称为应用层。这就是Web客户(浏览器)、Telnet客户、Web服务器、FTP服务器和其他我嫩使用的网络应用所在的层。对于网际协议,OSI模型的顶上三层协议几乎没有区别。

为什么套接字提供的是从OSI模型的顶上三层进入传输层的接口?

  1. 顶上三层处理具体网络应用(如FTP、Telnet或HTTP)的所有细节,却对通信细节了解很少;底下四层对具体网络应用了解不多,却处理所有的通信细节:发送数据,等待确认,给无序到达的数据排序,计算并验证校验和,等等。
  2. 顶上三层通常构成所谓的用户进程(user process),底下四层却通常作为操作系统内核的一部分提供。
  3. 由此可见,第4层和第5层之间的接口是构建API的自然位置。

netstat -i 提供网络接口信息,环回(loopback)接口称为lo
netstat -r 展示路由表,MULTICAST标志通常指明该接口所在主机支持多播。

在这里插入图片描述

常见函数及变量

可变参数va_list、va_start、vsprintf、va_end

在ANSI C中,这些宏的定义位于stdarg.h中,典型的实现如下:
va_list是一个字符指针,存储参数地址,因为得到参数的地址之后,再结合参数的类型,才能得到参数的值

typedef char* va_list;

va_start宏,获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数):

#define va_start(list,param1)   ( list = (va_list)&param1+ sizeof(param1) )

在这里插入图片描述
va_arg宏,返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。(mode参数描述了当前参数的类型):

#define va_arg(list,mode)   ( (mode *) ( list += sizeof(mode) ) )[-1]
它必须返回一个由va_list所指向的恰当的类型的数值,同时递增va_list,使它指向参数列表中的一个参数(即递增的大小等于与va_arg宏所返回的数值具有相同类型的对象的长度)。因为类型转换的结果不能作为赋值运算的目标,所以va_arg宏首先使用sizeof来确定需要递增的大小,然后把它直接加到va_list上,这样得到的指针再被转换为要求的类型。因为该指针现在指向的位置"过"了一个类型单位的大小,所以我们使用了下标-1来存取正确的返回参数。

va_end宏,清空va_list可变参数列表:

#define va_end(list) ( list = (va_list)0 )

注:以上sizeof()只是为了说明工作原理,实际实现中,增加的字节数需保证为**int的整数倍**
如:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
说明:
对于两个正整数 x, n 总存在整数 q, r 使得

x = nq + r, 其中  0<= r <n                  //最小非负剩余

q, r 是唯一确定的。q = [x/n], r = x - n[x/n]. 这个是带余除法的一个简单形式。在 c 语言中, q, r 容易计算出来:

    q = x/n, r = x % n.

所谓把 x 按 n 对齐指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 这也相当于把 x 表示为:

    x = nq + r', 其中 -n < r' <=0                //最大非正剩余   

nq 是我们所求。关键是如何用 c 语言计算它。由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:

x+n = qn + (n+r'),其中 0<n+r'<=n            //最大正剩余

x+n-1 = qn + (n+r'-1), 其中 0<= n+r'-1 <n    //最小非负剩余

所以 qn = [(x+n-1)/n]n. 用 c 语言计算就是:

((x+n-1)/n)*n

若 n 是 2 的方幂, 比如 2^m,则除为右移 m 位,乘为左移 m 位。所以把 x+n-1 的最低 m 个二进制位清 0就可以了。得到:

(x+n-1) & (~(n-1))

使用流程如下

#include <stdarg.h>
va_list arg_ptr;
va_start(arg_ptr, format);
slen = vsprintf(buf, format, arg_ptr);
va_end(arg_ptr);
  • 调用参数表之前,应该定义一个va_list类型的变量,以供以后用(arg_ptr);
  • 然后对arg_ptr初始化,让它指向可变参数表里面的第一个参数。这是通过va_start来实现的,第一个参数是arg_ptr,第二个参数是在变参表前面紧挨着的一个变量。
  • 然后用va_arg获取参数,va_arg的第二个参数是要获取的参数的指定类型,并返回这个指定类型的值,同时把arg_ptr的位置指向变参表的下一个变量位置;
  • 获取所有的参数之后,我们有必要将这个arg_ptr指针关掉,以免发生危险,方法是调用va_end。它是将输入的参数arg_ptr值为NULL,应该养成获取完参数表之后关闭指针的习惯。
/*
函数名:vsprintf
功能:送格式化输出到串中
返回值:正常情况下返回生成字串的长度(除去\0),错误情况下返回负值
*/
int vsprintf(char *string, char *format, va_list param);

代码举例:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
char buffer[80] = {0};
int vsp(char *fmt,...)
{
va_list argptr;
int count;
va_start(argptr, fmt);
count = vsprintf(buffer, fmt, argptr);
va_end(argptr);
return count;
}
int main(int argc, char **argv)
{
int n = 30;
char string[4] = "abc";
vsp("%d %s",n , string);
printf("%s\n",buffer);
exit(0);
}

fflush()函数

#include <stdio.h>
int fflush(FILE *stream)
函数说明:fflush()会强迫将缓冲区内的数据写回参数stream指定的文件中,如果stream为NULLfflush()会将所有打开的文件数据更新

Linux errno

Linux中系统调用错误都存储于errno中,errno由操作系统维护,存储就近发生的错误,即下一次的错误会覆盖掉上一次的错误。 Ps:只有当系统调用或调用lib函数时出错,才会置位errno。
errno的值只在函数发生错误时设置,如果函数不返回错误,errno就没有定义。errno的所有正数错误值都是常值,具有以“E”开头的全大写字母名字,并通常在<sys/errno.h>头文件中定义。
1)打印错误信息 perror

#include <>
void perror(const char *s);
//打印系统错误信息

2)字符串显示错误信息 strerror

char *strerror(int errnum);
//返回错误码字符串信息

linux中stdout,stdin,stderr意义

stdout —— 标准输出
stdin —— 标准输入
stderr —— 标准错误
在Linux下,当一个用户进程被创建的时候,系统会自动为该进程创建上面的三个数据流,那么什么是数据流呢?我们知道,一个程序要运行,需要有输入、输出,如果出错,还要能表现出自身的错误,这时就要从某个地方读入数据、将数据输出到某个地方,这就构成了数据流。

int main(){
fprintf(stdout, "Hello");
fprintf(stderr, "world!");
return 0;
}

输出为:

world!Hello

这是为什么呢?在默认情况下,stdout是行缓冲区,他的输出会放在一个buffer中,只有到换行的时候才会输出到屏幕,而stderr是无缓冲的,会直接输出,举例来说就是printf(stdout, “xxx”)和printf(“stdout,”xxx\n””),前者会憋住,直到遇到新行才会一起输出。而printf(stderr,“xxxx”)不管有没有\n,都有输出。

exit(0)与exit(1),return三者区别(详解)

exit(0):正常运行程序并退出程序;
exit(1):非正常运行导致退出程序;
return():返回函数,若在主函数中,则会退出函数并返回一值。
详细说:

  1. return返回函数值,是关键字; exit 是一个函数。
  2. return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
  3. return是函数的退出(返回);exit是进程的退出。
  4. return是C语言提供的,exit是操作系统提供的(或者函数库中给出的)。
  5. return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出。
  6. 非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。

exit和_exit
进程终止有5种方法:
1正常终止
(1)从main函数返回
(2)调用exit
(3)调用_exit
2异常终止
(1)调用abort
(2)由一个信号来终止
exit和_exit就是用来正常终止一个进程的,主要区别是_exit会立刻进入内核,而exit先执行一些清除工作(包括执行各种终止处理程序,关闭所有标准I/O等,一旦关闭了IO,例如Printf等函数就不会输出任何东西了),然后才进入内核。这两个函数会对父子进程有一定的影响,当用vfork创建子进程时,子进程会先在父进程的地址空间运行(这跟fork不一样),如果子进程调用了exit就会把父进程的IO给关掉。

这两个函数都带一个参数表示终止状态,这跟我们平时写的return效果是一样的,如果不返回一个终止状态,那表示这个进程的终止状态就是未定义的。

fileno()

功 能:把文件流指针转换成文件描述符
相关函数:open, fopen
表头文件:#include <stdio.h>
定义函数:int fileno(FILE *stream)
函数说明:fileno()用来取得参数stream指定的文件流所使用的文件描述符
返回值 :返回和stream文件流对应的文件描述符。如果失败,返回-1。

#include <stdio.h>
main()
{
     FILE   *fp;
     int   fd;
     fp = fopen("/etc/passwd", "r");
     fd = fileno(fp);
     printf("fd = %d\n", fd);
     fclose(fp);
}

文件描述词是Linux编程中的一个术语。当一个文件打开后,系统会分配一部分资源来保存该文件的信息,以后对文件的操作就可以直接引用该部分资源了。文件描述词可以认为是该部分资源的一个索引,在打开文件时返回。在使用fcntl函数对文件的一些属性进行设置时就需要一个文件描述词参数。
以前知道,当程序执行时,就已经有三个文件流打开了,它们分别是标准输入stdin,标准输出stdout和标准错误输出stderr。和流式文件相对应的是,也有三个文件描述符被预先打开,它们分别是0,1,2,代表标准输入、标准输出和标准错误输出。
需要指出的是,上面的流式文件输入、输出和文件描述符的输入输出方式不能混用,否则会造成混乱。

write()和read()函数

write()

头文件:<unistd.h>
ssize_t write(int fd, const void *buf, size_t nbyte);
fd:文件描述符;
buf:指定的缓冲区,即指针,指向一段内存单元;
nbyte:要写入文件指定的字节数;
返回值:写入文档的字节数(成功);-1(出错)
write函数把buf中nbyte写入文件描述符fd所指的文档,成功时返回写的字节数,错误时返回-1,并设置errno变量.
在网络程序中,当我们向套接字文件描述符写时有俩种可能:

  1. write的返回值大于0,表示写了部分或者是全部的数据.
  2. 返回的值小于0,此时出现了错误.我们要根据错误类型来处理.2)返回的值小于0,此时出现了错误.我们要根据错误类型来处理.如果错误为EINTR表示在写的时候出现了中断错误. 如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接).

read()

函数原型:ssize_t read(int fd, void *buf, size_t count);
返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0。
参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。
read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。

fgets()和fputs()

fgets()

函数的原型如下:char *fgets(char *buf, int n, FILE *fp)
功能:从文件流读取一行,送到缓冲区,使用时注意以下几点:
1.当遇到换行符或者缓冲区已满,fgets就会停止,返回读到的数据,值得注意的是不能用fgets读二进制文件,因为fgets会把二进制文件当成文本文件来处理,这势必会产生乱码。
2.每次调用,fgets都会把缓冲区的最后一个字符设为null,这意味着最后一个字符不能用来存放需要的数据,所以如果有一行,含有LINE_SIZE个字符(包括换行符),要想把这行读入缓冲区,请把参数n设为LINE_SIZE+1
3. 由结论1可推出:给定参数n,fgets只能读取n-1个字符(包括换行符),如果有一行超过n-1个字符,那么fgets返回一个不完整的行,也就是说,只读取该行的前n-1个字符,但是,缓冲区总是以null字符结尾,对fgets的下一次调用会继续读该行。

fputs()

int fputs(const char *str, FILE *stream);
返回值:该函数返回一个非负值,如果发生错误则返回 EOF(-1)。
(1)str:这是一个数组,包含了要写入的以空字符终止的字符序列。
(2)sstream:指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流

编译问题

警告: 隐式声明与内建函数 ‘exit’ 不兼容

process41.c: 在函数 ‘main’ 中:
process41.c:17: 警告: 隐式声明与内建函数 ‘exit’ 不兼容
process41.c:19: 警告: 隐式声明与内建函数 ‘sprintf’ 不兼容
process41.c:32: 警告: 隐式声明与内建函数 ‘exit’ 不兼容
process41.c: 在函数 ‘func’ 中:
process41.c:36: 警告: 隐式声明与内建函数 ‘printf’ 不兼容
加入这两个头文件就可以了!
#include <stdio.h>
#include <stdlib.h>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值