网络编程技术期末考试复习(计算机网络期末考试)【自用】

课程内容概览

讲授内容 😁
  1. 基本的套接口函数和结构体的定义
  2. TCP客户程序的基本结构 迭代服务器并发服务器的基本结构
  3. UDP客户程序的基本结构 UDP服务器程序的基本结构
  4. I/O 复用技术 多客户连接的tcp服务器程序基本结构 tcp/udp多协议服务器程序的基本结构
  5. 主机名与ip地址转换 服务器与端口号的转换
教材

UNIX网络编程

ch1 一个简单的客户-服务端程序

概述

基于套接口的tcp/udp网络应用程序基本结构 函数及其程序设计

客户端代码分析 👀
#include "unp.h" //史蒂文斯的头文件

int main(int argc,char **argv) 
/* argc--argv中的字符串个数
	int main(int argc,char *argv[])
*/
{
	int sockfd, n; //sockfd套接口文件 
	char recvline[MAXLINE+1] ; //recieve line 注意 \0的存在,so:+1
	struct sockaddr_in servaddr; //sockaddr_in

	/*
		argc == 2 
		./文件 地址
		
	*/
	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(2013);
	
	/*
		inet_pton
		inet_pton和inet_ntop函数
		这两个函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达
		presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到
		套接字地址结构的二进制值。

		AF_INET,argv[1] --->  servaddr.sin_addr (存放二进制结果形式)
	*/
	
	if (inet_pton(AF_INET,argv[1],&servaddr.sin_addr)<=0)
		err_quit("inet_pton error for %s",argv[1]);
		
	/*
		int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
		套接字连接服务器地址 --- 内部进行三次握手,失败返回-1
	*/
	if (connect(sockfd,(SA * )&servaddr,sizeof(servaddr))<0)
		err_sys("connect error");
		
	while((n = read(sockfd,recvline,MAXLINE))>0){
	
		//这一行代码从套接字 `sockfd` 中读取数据,最多读取 `MAXLINE` 个字节,并将其存储到 `recvline` 数组中。`n` 变量存储了实际读取的字节数
	
		recvline[n] = 0;
		//在 `recvline` 数组中的第 `n` 个字节后面添加一个空字符 `'\0'`,以确保接收到的数据在末尾被正确地标记为字符串的结束。
		if (fputs(recvline,stdout) == EOF)
			err_sys("fputs error");
		//使用 `fputs` 函数将 `recvline` 中的数据输出到标准输出 `stdout` 上。如果 `fputs` 函数返回 `EOF`(表示出错),则会调用 `err_sys("fputs error")` 来输出错误信息并退出程序。
	}
	if (n<0)
		err_sys("read error");

	exit(0);
	//一旦程序执行到这一行,它将退出 `main` 函数,进而退出整个程序。这样做是为了在程序执行完毕后正常地终止程序的运行。
}

出错函数: err_quit, err_sys

出错函数汇总
内部调用了err_doit()

/**
 * 打印消息, 并且返回调用者
 * 调用者指定参数errnoflag
 * @param errnoflag 错误标识, 值非0时, 才打印错误号转化成的错误详细信息. 值为0时, 不打印错误号对应错误详细信息.
 * @param error 错误号. 系统调用相关错误, 不用传错误号, 直接用errno作为错误号; 非系统调用错误(不设置errno的), 手动传入错误号
 * @param fmt 格式化字符串
 * @param ap 变参列表, fmt中有多少个转义字符, ap就需要提供同样个数参数值
 */
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap)
{
	int		errno_save;
	char	buf[MAXLINE];

	errno_save = error;		/* value caller might want printed */
	vsprintf(buf, fmt, ap); /* format string fmt write out to buf */
	if (errnoflag)
		sprintf(buf+strlen(buf), ": %s", strerror(errno_save)); 
		/* append strerror(error) to buf */
	strcat(buf, "\n"); /* append "\n" to buf */
	fflush(stdout);		/* in case stdout and stderr are the same */
	fputs(buf, stderr); /* write out buf to stderr */
	fflush(stderr);		/* SunOS 4.1.* doesn't grok NULL argument */
}

socket

int socket(int af, int type, int protocol);
/*
* af address family 地址族 ipv4/v6
* type 传输类型 SOCK_STREAM(流格式套接字/面向连接的套接字)和 SOCK_DGRAM(数据报套接字/无连接的套接字)面向连接的套接字--一般是传输层tcp协议 无连接的套接字--一般是传输层udp协议
*
*/

struct sockaddr_in

sockaddr_in 是一个用于表示 IPv4 地址和端口的结构体,在网络编程中经常用于指定套接字地址。在 C 语言中,它的定义通常如下:

struct sockaddr_in {     
	sa_family_t    sin_family; // 地址族,通常为 AF_INET     
	in_port_t      sin_port;   // 端口号,使用网络字节序(big-endian)     
	struct in_addr sin_addr;   // IPv4 地址     
	char           sin_zero[8]; // 为了保持与 sockaddr 结构体大小相同而填充的空字节 
};

其中:

  • sin_family:表示地址族,通常为 AF_INET,代表 IPv4 地址族。
  • sin_port:表示端口号,使用网络字节序(即大端序)存储。在网络编程中,常常需要使用 htons() 函数将主机字节序转换为网络字节序。
  • sin_addr:是一个 struct in_addr 类型的结构体,用于存储 IPv4 地址。in_addr 结构体定义如下:
struct in_addr {     
	uint32_t       s_addr;     // IPv4 地址,使用网络字节序(big-endian) 
};
  • sin_zero:是一个填充字段,用于保持 sockaddr_in 结构体与通用套接字地址结构 sockaddr 的大小相同。在实际应用中通常不使用它,可用 memset() 函数将其置为零。

这个结构体提供了一种方便的方式来存储和处理 IPv4 地址和端口号。在网络编程中,常常用于在套接字 API 中指定服务器和客户端的地址信息。

服务端代码分析 👀
#include "unp.h"
#include <time.h>

int main(int argc,char **argv)
{
	int     listenfd,connfd; 
	struct sockaddr_in   servaddr; //自己的地址
	char    buff[MAXLINE]; 
	time_t  ticks; //系统时间

	char* temp_str = "Hello,future!"; 

	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(2013); //端口-> 2013 (不同端口具有不同作用--一个软件会监听多个端口)
	
	//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
	
	Bind(listenfd,(SA *) &servaddr,sizeof(servaddr));
	Listen(listenfd,LISTENQ); //listenq -> listen queue
	
	//listenfd 连接到servaddr上,监听客户端的请求, connfd接收客户端的请求来创建连接,从而控制客户端
	for(;;){
		connfd = Accept(listenfd,(SA*)NULL,NULL); // 与客户端通信

		ticks = time(NULL);
		//snprintf(buff,sizeof(buff),"%.24s\r\n",ctime(&ticks));
		snprintf(buff,sizeof(buff),"%s\n",temp_str); //sprintf->输入到串
		Write(connfd,buff,strlen(buff));

		Close(connfd);
	}
}

bind函数和listen函数形象化理解

想象一下你打算在大家庭聚会中安排座位。每个人都有一个座位号,这就像端口号一样。而你想要给每个人一个座位,确保他们坐得舒适,这就像bind()函数所做的事情,它将套接字(socket)与特定的端口关联起来。

一旦你安排好座位,你会开始听从别人的请求:他们可能会问你是否可以借用某个东西,或者请求你帮忙。这就像listen()函数的作用,它告诉你的程序开始监听指定端口上的连接请求。

而当有人想要与你交谈时,他们会通过你的座位号来找到你,然后你会与他们交流。这就像客户端尝试连接到你的服务器套接字,然后服务器通过accept()函数来接受连接并与客户端建立通信。

端口理解

不同端口具有不同作用,不同端口通常用于不同的服务或应用程序。这样做的原因是为了确保在同一台计算机上可以同时运行多个网络服务,并通过端口号将数据包路由到正确的应用程序。

举个例子,常见的端口使用方式包括:

  1. 80端口:通常用于HTTP服务,即网页浏览器通过HTTP协议与Web服务器通信时使用的端口。当你在浏览器中输入网址并按下回车键时,浏览器会默认连接到目标服务器的80端口,除非另有指定(例如HTTPS默认端口443)。

  2. 443端口:通常用于HTTPS服务,即通过加密的HTTP协议传输数据的安全网页浏览。访问加密网站时,浏览器会连接到目标服务器的443端口。

  3. 22端口:通常用于SSH服务,用于安全远程访问和文件传输。系统管理员可以通过SSH连接到远程服务器来执行命令或管理服务器。

  4. 25端口:通常用于SMTP服务,即简单邮件传输协议,用于发送电子邮件。

  5. 21端口:通常用于FTP服务,即文件传输协议,用于在客户端和服务器之间传输文件。

每个端口都有其指定的默认用途,但实际上可以配置服务器以在不同端口上提供不同的服务。

协议无关性

协议无关性(Protocol Independence)是指在软件设计中,特定功能的实现与底层协议的选择无关。换句话说,这意味着编写的软件能够在不同的网络协议之间进行切换而不需要修改其核心逻辑。

在网络编程中,==协议无关性通常指的是在处理网络通信时,程序的逻辑与使用的网络协议无关。==这意味着无论是TCP、UDP还是其他协议,程序的核心逻辑都不受影响。

协议无关性的实现可以通过以下几种方式:

  1. 抽象接口:使用抽象接口来隐藏底层协议的细节,使得不同的协议可以被透明地切换。这样的接口可以提供一致的操作方法,无论使用何种协议。

  2. 通用数据结构:使用通用的数据结构来表示网络数据,使得数据在不同的协议之间进行转换时更加方便。比如使用通用的地址结构来表示不同协议的地址信息。

  3. 协议独立的函数库:编写支持多种协议的函数库,使得开发者可以直接调用这些函数而不必关心底层协议的选择。

协议无关性的优点包括:

  • 可移植性:由于与特定协议的绑定较少,因此代码更容易在不同的平台上进行移植。
  • 灵活性:能够适应不同的网络环境和需求,使得软件更加灵活和通用。
  • 维护性:降低了代码的耦合度,使得修改和维护变得更加容易。

在实际编程中,协议无关性通常是一个重要的设计目标,特别是对于需要跨平台或需要与多种协议进行交互的软件。

⁉ 🤔 或许socket套接字的使用就是协议无关性的一种体现。

包裹函数–错误处理

socket() 和 Socket()

客户端代码段
	if ((sockfd = socket(AF_INET,SOCK_STREAM,0))<0)
		err_sys("socket error:");
服务端代码段
	listenfd = Socket(AF_INET,SOCK_STREAM,0);

可见Socket可以比socket少写一个if语句,代码如下。

int
Socket(int family,int type, int protocol)
{
	int n;
	if ((n = socket(family,type,protocol)) < 0)
		err_sys("socket errror");
	return n;
}
其他补充

服务器的监听窗口和客户连接端口保持一致,使用htons和ntohs进行字节转换

套接口创建和使用的流程

  1. 创建套接口:程序通过调用操作系统提供的套接口创建函数(如 socket()),来创建一个套接口。
  2. 绑定地址和端口:服务器端程序通过调用 bind() 函数将套接口绑定到一个特定的 IP 地址和端口号上。
  3. 监听连接请求(服务器端):服务器端程序通过调用 listen() 函数开始监听来自客户端的连接请求。
  4. 发起连接请求(客户端):客户端程序通过调用 connect() 函数连接到服务器端的套接口。
  5. 接受连接(服务器端):服务器端程序通过调用 accept() 函数接受来自客户端的连接请求,并返回一个新的套接口用于与客户端通信。
  6. 数据交换:连接建立后,两个套接口对之间可以通过发送和接收数据进行通信。
  7. 关闭连接:通信完成后,程序通过调用 close() 函数关闭套接口。

ch2 传输层TCP和UDP

概述

传输层协议
tcp–可靠
udp–不可靠 简单

UDP:用户数据报协议

TCP:传输控制协议

每一个字节关联一个序列号进行排序

TCP连接的建立和终止

tcp首部

TCP 首部是 TCP(Transmission Control Protocol,传输控制协议)头部,用于 TCP 数据报的头部信息。它位于 IP 头部之后,TCP 数据部分之前。TCP 首部包含了一系列字段,用于控制和管理 TCP 连接的建立、维护和关闭。以下是 TCP 首部的常见字段:

  1. 源端口号(Source Port):16 位,指明发送端的端口号。
  2. 目标端口号(Destination Port):16 位,指明接收端的端口号。
  3. 序列号(Sequence Number):32 位,用于对字节流进行编号,用于保证数据的顺序性。
  4. 确认号(Acknowledgment Number):32 位,如果 ACK 标志位被置位,则确认号字段包含下一个期望收到的序列号。
  5. 数据偏移(Data Offset):4 位,指明 TCP 首部的长度,以 4 字节为单位。通常,TCP 首部的长度是可变的,因为 TCP 首部中可能包含有可选字段,如选项字段。
  6. 保留(Reserved):6 位,保留供将来使用,目前置 0。
  7. 控制标志(Flags):6 位,用于控制 TCP 连接的建立、维护和关闭,包括 SYN、ACK、FIN、RST、PSH 和 URG 等标志位。
  8. 窗口大小(Window Size):16 位,指明接收端的窗口大小,用于流量控制。
  9. 校验和(Checksum):16 位,用于对 TCP 首部和数据部分进行校验,以保证数据的完整性。
  10. 紧急指针(Urgent Pointer):16 位,仅当 URG 标志位被置位时,紧急指针才有效,指示紧急数据的末尾位置。
  11. 选项(Options):0 至 320 位,可选字段,用于实现一些特殊功能,如选择确认、最大报文段长度等。1 .mss选项–tcp分节(如syn …) 2.窗口规模选项 3.时间戳选项
    TCP 首部的总长度为 20 字节(不包括选项字段),但在有选项字段存在时,可以通过 Data Offset 字段来确定其具体长度。

tcp首部

三路握手

三路握手

四次挥手 (四分组交换)

ch2-四次挥手

TIME__WAIT状态

见上面四次挥手图片。
TIME_WAIT是客户端进入的状态–在接收到服务器端的FIN N确认关闭信号之后,发出ack N+1进入TIME_WAIT状态,通知服务器已经关闭。防止ack N+1丢失

所以:存在TIME_WAIT状态有两个理由:
1. 实现终止TCP全双工连接的可靠性
2. 允许老的重复分节在网络中消逝

端口号

端口号的分配

端口号的分配

套接口对

套接口(Socket)对是指在网络编程中使用的一种通信机制,它是通信端点的抽象。一个套接口对包括了通信协议、本地 IP 地址、本地端口号、远程 IP 地址以及远程端口号等信息。在套接口对中,有两个套接口,分别位于通信的两端,一个用于发送数据,另一个用于接收数据。

标识每个端点的两个值(IP地址和端口号)通常称为一个套接口。

TCP端口号与并发服务器

并发服务器中主服务器循环派生子进程来处理每个新的连接。

ch3 套接口编程简介

套接口地址结构

IPV4套接字地址结构

struct in_addr {
	in_addr_t s_addr;
};

struct sockaddr_in{
	uint8_t sin_len;//结构体变量长度
	sa_family_t sin_family;
	in_port sin_port;
	struct in_addr sin_addr;
	char sin_zero[8];
};

通用套接口地址结构❗

struct sockaddr{
	uint8_t sa_len;
	sa_family_t sa_family;
	char sa_data[14]
};

IPV6

ch3-ipv6套接字地址结构

值-结果参数

理解

参数作为函数的值输入,并且作为函数的的结果发生改变。

def modify_value_result(param):

    param[0] += 10  # 修改参数值

  

value = [5]  # 定义一个可变对象,以模拟值-结果参数


print("Before function call:", value[0])  # 输出函数调用前的值5

modify_value_result(value)  # 调用函数

print("After function call:", value[0])  # 输出函数调用后的值15

结果如下

Before function call: 5
After function call: 15

对应函数

bind connect sendto

字节排序函数

host主机字节序–小端
net网络字节序–大端
htons
host to network short(16bit)
htonl
host to network long(32bit)
ntohs
ntohl

字节操纵函数

字节操纵函数是用于对内存中的数据进行字节级别操作的函数。这些函数通常在 C 标准库中定义,可以用于处理字符串、二进制数据等。以下是一些常见的字节操纵函数:

  1. memset:用于将指定的内存区域的前 n 个字节设置为特定的值。

    • 原型:void *memset(void *s, int c, size_t n);
    • 示例:memset(buffer, 0, sizeof(buffer));
  2. memcpy:用于从源内存地址复制 n 个字节到目标内存地址。

    • 原型:void *memcpy(void *dest, const void *src, size_t n);
    • 示例:memcpy(dest, src, sizeof(src));
  3. memmove:类似于 memcpy,但在重叠的内存区域内安全地复制数据。

    • 原型:void *memmove(void *dest, const void *src, size_t n);
    • 示例:memmove(dest, src, sizeof(src));
  4. memcmp:用于比较两个内存区域的前 n 个字节。

    • 原型:int memcmp(const void *s1, const void *s2, size_t n);
    • 示例:if (memcmp(buf1, buf2, sizeof(buf1)) == 0) { /* 两个缓冲区相等 */ }
  5. bzero:已废弃的函数,用于将指定的内存区域全部设置为0。

    • 原型:void bzero(void *s, size_t n);
    • 示例:bzero(buffer, sizeof(buffer));
  6. bcopy:已废弃的函数,类似于 memcpy,用于从源内存地址复制 n 个字节到目标内存地址。

    • 原型:void bcopy(const void *src, void *dest, size_t n);
    • 示例:bcopy(src, dest, sizeof(src));
  7. bcmp:已废弃的函数,用于比较两个内存区域的前 n 个字节。

    • 原型:int bcmp(const void *s1, const void *s2, size_t n);
    • 示例:if (bcmp(buf1, buf2, sizeof(buf1)) == 0) { /* 两个缓冲区相等 */ }
地址转换函数

地址转换函数用于将不同格式的地址进行转换,常见的包括将 IP 地址和主机字节序与网络字节序之间进行转换。在网络编程中,这些函数通常用于处理套接字地址。

以下是一些常见的地址转换函数:

  1. htonl(Host to Network Long):将 32 位的主机字节序整数转换为网络字节序整数。

    • 原型:uint32_t htonl(uint32_t hostlong);
  2. htons(Host to Network Short):将 16 位的主机字节序短整数转换为网络字节序短整数。

    • 原型:uint16_t htons(uint16_t hostshort);
  3. ntohl(Network to Host Long):将 32 位的网络字节序整数转换为主机字节序整数。

    • 原型:uint32_t ntohl(uint32_t netlong);
  4. ntohs(Network to Host Short):将 16 位的网络字节序短整数转换为主机字节序短整数。

    • 原型:uint16_t ntohs(uint16_t netshort);

这些函数通常用于处理套接字地址中的 IP 地址和端口号,在将地址数据发送到网络上或者从网络接收数据时需要进行转换,以确保数据在不同主机之间能够正确解释。

readn、writen和readline函数
#include "unp.h"
ssize_t readn(int filedes,void * buff,size_t nbytes);
//和read区别不大
ssize_t writen(int filedes,const void * buff,size_t nbytes);
//和write区别不大
ssize_t readline(int filedes,void * buff,size_t maxlen);

//filedes通常用于表示文件描述符(file descriptor)
isfdtype函数

测试一个描述子是不是某个给定类型

isfdtype(int fd,int fdtype);

例如

isfdtype(sockfd,S_IFSOCK);

fdtype

在计算机网络编程中,常见的文件描述符类型(fdtype)包括:

  1. 普通文件描述符:用于表示普通文件的文件描述符。这些文件描述符用于打开、读取和写入文件。
  2. 套接字描述符:用于表示网络套接字的文件描述符。在网络通信中,套接字描述符用于建立和管理网络连接,进行数据的发送和接收。
  3. 管道描述符:用于表示管道的文件描述符。管道描述符用于在进程之间进行通信,允许一个进程将数据写入管道,另一个进程从管道中读取数据。
  4. 标准输入/输出描述符:用于表示标准输入、标准输出和标准错误流的文件描述符。在UNIX系统中,通常用整数0表示标准输入(stdin)、整数1表示标准输出(stdout),整数2表示标准错误(stderr)。
  5. 设备描述符:用于表示设备文件的文件描述符。设备描述符用于与硬件设备进行交互,例如磁盘驱动器、串口、打印机等。

ch4 基本TCP套接口编程

基本套接口函数

socket函数

connect函数

#include <syslsocket.h>  
int connect(int sockfd, const struct sockaddr * servaddr,socklen_t addrlen); 

RST的含义为“复位”,它是TCP在某些错误情况下所发的一种TCP分节。有三个条件可以产生RST:

bind函数

给套接口分配一个本地协议地址

int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen)

返回 0 -->成功 -1 出错

ch4-bind函数

  1. 通配地址(Wildcard Address):在计算机网络中,通配地址是指一个可以匹配多个具体地址的特殊地址。在IPv4中,通配地址通常表示为0.0.0.0,在IPv6中通常表示为::。通配地址在网络编程中经常用于指示某个套接字可以接受来自任意IP地址的连接。

  2. 端口(Port):在网络通信中,端口用于标识一个进程或服务。端口号是一个16位的数字,范围从0到65535。0到1023之间的端口号被预留用于一些众所周知的服务,比如HTTP的端口号是80,HTTPS的端口号是443等。

ip通配地址—未知–内核选择
端口—0—未知—内核选择

listen 函数

函数的第二个参数规定了内核为此套接口排队的最大连接个数。

#include <sys/socket.h>
int listen(int sockfd, int backlog);

返回:0——成功,-1——出错

void Listen(int fd, int backlog)
{
	char *ptr;
	if ( (ptr =getenv("LISTENQ"))!= NULL) 
	// getenv("LISTENQ") 环境变量中LISTENQ的值 如果不为null,不为空
		backlog = atoi(ptr); 	//字符ascll to int
		
	if (listen(fd, backlog)<0)
		err_sys("listen error");
}

accept 函数

accept由TCP服务器调用从已完成连接队列头返回下一个已完成连
接。若已完成连接队列为空,则进程睡眠。【假定套接口为缺省的阻塞方式】

include <syslsocket.h>
int
accept(int sockfd, struct sockaddr * cliaddr, socklen_t *addrlen);

返回:(非负描述字)—OK,-1——出错

参数cliaddr和addrlen用来返回连接对方进程(客户)的协议地址。addrlen是值结果参数:调用前,我们将由*addlen所指的整数值置为由cliaddr所指的套接口地址结构的长度,返回时,此整数值即为由内核存在此套接口地址结构内的准确字节数

如果函数accept执行成功,则返回值是由内核自动生成的一个全新描述字,代表与客户的TCP连接。当我们讨论函数accept时,常把它的第一个参数称为监听套接口(listening socket)描述字(由函数socket生成的描述字,用作函数bind和listen的第一个参数),把它的返回值称为已连接套接口(connected socket)描述字。

并发服务器

pid_t fork(void);
返回:子进程中返回0 父进程中返回子进程id , -1出错

ch4-并发服务器代码

close函数

标记这个套接口描述子不再为此进程所用

ch4-描述字访问计数

getsockname和getpeername函数
#include <syslsocket.h>
int getsockname(int sockfd, struct sockaddr *localaddr,socklen_t * addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr,socklen_t * addrlen);
两者均返回:0—OK,-1——出错
这是值结果参数的形式

这两个函数或返回与套接口关联的本地协议地址(getsockname),或返回与套接口关联的远程协议地址(getpeername) 。

send 和recv函数
ssize_t send (int sockfd, const void *buf, size_t len, int flags);
ssize_t recv (int sockfd, const void *buf, size_t len, int flags);

ch5 TCP客户-服务器程序例子 ⏫

概述

ch5-概述

TCP回射服务器-客户程序 👀

概览

ch5-程序

注 : ./服务端程序 & → {\to} 表示后台运行

服务端

#include "unp.h"

int 
main(int argc,char ** argv){
	int listenfd, connfd;
	struct sockaddr_in servaddr, cliaddr;
	pid_t childpid;
	socklen_t clilen;
	void sig_chld(int);

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	
	Bind(listenfd,(SA* )&servaddr,sizeof(seraddr));
	Listen(listen,LISTENQ);
	Signal(SIGCHLD,sig_chld);
	for(;;){
		//connfd = Accept(listenfd,(SA* )&cliaddr,&clilen);
		if((connfd = accept(listenfd,(SA* )&cliaddr,&clilen)) < 0){
			if (errno == EINTR)
				continue;
			else
				err_sys("accept error");
		}
		if ((childpid = Fork()) == 0){
			Close(listenfd);
			str_echo(connfd); // 
			Close(connfd);
			exit(0);
		}
		Close(connfd);
	}
	
}
void
sig_chld(int signo){
	int stat;
	pid_t pid;
	while((pid = waitpid(-1,&stat,WNOHANG)) > 0){
		printf("childpid %d terminated",pid);
	}
	return;
}
#include    "unp.h"

void
str_echo(int sockfd)
{
    ssize_t     n;
    char        line[MAXLINE];
    for ( ; ; ) {
        if ( (n = Readline(sockfd, line, MAXLINE)) == 0)
            return;     /* connection closed by other end */
        Writen(sockfd, line, n);
    }
}

客户端

#include "unp.h"

int 
main(int argc,char ** argv){
	int sockfd;
	struct sockaddr_in servaddr;
	if(argc != 2)
		err_quit("usage <IPaddress>");

	sockfd = Socket(AF_INET,SOCK_STREAM,0);
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	Inet_pton(AF_INET,argv[1],&servaddr.sin_addr);

	Connect(sockfd,(SA*) &servaddr,sizeof(servaddr));
	str_cli(stdin,sockfd);
	exit(0);
}
#include "unp.h"
void
str_cli(File *fp,int sockfd){
	char recvline[MAXLINE], sendline[MAXLINE];
	while(Fgets(sendline,MAXLINE,fp) != NULL){ //一直等待用户输入
		Writen(sockfd,sendline,strlen(sendline));

		if (Readline(sockfd,recvline,MAXLINE) == 0){
			err_quit("server terminated");
		}
		Fputs(recvline,stdout);
	}
}
正常启动与终止

启动

客户端 -->socket和connect --> 引起tcp的三路握手过程 --> 握手结束,connect返回客户,accept返回服务器。连接建立。

–>客户调用str_cli函数该函数阻塞于fgets调用(我们未输入文本)
–>accept返回服务器之后,服务端调用fork。子进程调用str_echo。调用readline而阻塞
–>服务器父进程再次调用accept而阻塞,等待下一个用户连接。

客户进程,服务器父进程,服务器子进程 – 都在阻塞(睡眠)

终止

ch5-终止1
ch5-终止2

Posix信号处理

信号是发生某件事时对进程的通知,有时称为软中断。它一般是异步的,这就是说,进程不可能提前知道信号发生的时间

信号处理的方法Posix方法–sigaction函数
简单的信号处理函数–signal

处理SIGCHLD信号

僵尸子进程的意义–保留子进程的某些信息,便于恢复

Signal(SIGCHLD,sig_chld) (放在listen之后)

wait和waitpid函数

pid_t wait( int * staloc);
pid_t waitpid(pid_t pid,int * statloc,int options);

区别

wait阻塞直到第一个现有子进程终止
waitpid对等待那个进程及其是否阻塞给了我们更多控制。pid是-1代表等待第一个终止的子进程。options最多是情况是WNOHANG(不要阻塞)

代码应用

#include "unp.h"

void
sig_chld(int signo){
	pid_t pid;
	int stat;
	while((pid = waitpid(-1,&stat,WNOHANG)) > 0){
		print("child %d teminated",pid);
	}
	return;
}

外面是SIgnal函数–>signal接收到信号后,调用sig_chld(),所以while(…>0)而不是>=0

程序运行异常处理⬇
数据格式

客户-服务文本格式

sscanf函数

客户-服务二进制格式

结构体 + cjson/json-c

ch6 I/O复用 select和poll函数 ⏫

概述
IO模型

阻塞I/O

一直等待数据

非阻塞I/O

循环查询是否有数据准备好

I/O复用模型

进程受阻于select调用,等待可能多个套接口中的任一一个变为可读

信号驱动I/O模型

异步I/O模型

各种模型比较

第一阶段(等待数据)处理不同
第二阶段处理相同(阻塞于recvfrom调用,recvfrom将内核数据转移到用户数据)

select函数

select 函数是一个在网络编程中常用的系统调用,用于实现多路复用 I/O。它允许程序监视多个文件描述符,等待其中任何一个变为可读、可写或者发生异常。一般来说,select 函数用于处理多个套接字的 I/O 事件,从而避免使用多个线程或进程来处理每个套接字的 I/O 事件。

以下是 select 函数的基本信息:

  • 原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 参数:
    • nfds:要监视的文件描述符的数量,通常设置为待监视的最大文件描述符加 1。
    • readfds:指向一个 fd_set 结构体的指针,用于指定要监视可读事件的文件描述符集合。
    • writefds:指向一个 fd_set 结构体的指针,用于指定要监视可写事件的文件描述符集合。
    • exceptfds:指向一个 fd_set 结构体的指针,用于指定要监视异常事件的文件描述符集合。
    • timeout:指定超时时间,即 select 函数在等待事件发生的最长时间。如果设置为 NULLselect 函数将一直阻塞,直到有事件发生;如果设置为一个不为 NULLtimeval 结构体指针,select 函数将在指定时间内返回。
  • 返回值:
    • 若有文件描述符就绪,返回就绪文件描述符的数量。
    • 若超时时间到或者出错,则返回 -1,错误代码保存在 errno 中。

fd_set 是一个位图,用于表示一组文件描述符,其定义通常如下:

#define FD_SETSIZE 1024 // 默认的 fd_set 大小

typedef struct fd_set {
    int fds_bits[FD_SETSIZE / (8 * sizeof(int))];
} fd_set;

select 函数将监视指定的文件描述符集合,一旦其中的任何一个文件描述符就绪(可读、可写或者发生异常),select 就会返回。此时可以通过检查相应的文件描述符集合来确定哪些文件描述符已经就绪。

示例代码:

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    fd_set readfds;
    FD_ZERO(&readfds); // 初始化 readfds
    FD_SET(STDIN_FILENO, &readfds); // 添加标准输入到 readfds

    struct timeval timeout;
    timeout.tv_sec = 5; // 超时时间 5 秒
    timeout.tv_usec = 0;

    int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
    if (ret == -1) {
        perror("select");
        return 1;
    } else if (ret == 0) {
        printf("Timeout\n");
        return 0;
    } else {
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
            printf("Data is available on stdin\n");
            // 在这里可以读取标准输入
        }
    }

    return 0;
}

此示例中,程序监视标准输入是否可读,如果在超时时间内标准输入可读,则打印提示信息。

批量输入
shutdown函数

shutdown 函数用于关闭套接字的一部分功能,它允许对套接字进行有序关闭,从而允许在不完全关闭套接字的情况下继续进行数据传输。在网络编程中,shutdown 函数通常与 close 函数配合使用,用于关闭套接字。

以下是 shutdown 函数的基本信息:

  • 原型:int shutdown(int sockfd, int how);
  • 参数:
    • sockfd:套接字描述符,标识要关闭的套接字。
    • how:关闭方式,可以取以下值:
      • SHUT_RD:关闭套接字的读功能,不再接收数据。
      • SHUT_WR:关闭套接字的写功能,不再发送数据。
      • SHUT_RDWR:同时关闭套接字的读和写功能。
  • 返回值:若成功则返回 0,若出错则返回 -1,错误代码保存在 errno 中。

shutdown 函数通常用于以下场景:

  1. 在关闭套接字之前,允许发送或接收尚未完成的数据。
  2. 在不完全关闭套接字的情况下,允许继续进行部分数据传输。
  3. 在某些情况下,对套接字的读写操作进行分离,允许更精细地控制数据流。

示例代码:

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

int main() {
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        return 1;
    }

    // 连接到服务器...

    // 关闭套接字的写功能,不再发送数据
    if (shutdown(sockfd, SHUT_WR) == -1) {
        perror("shutdown");
        return 1;
    }

    // 读取服务器返回的数据...
    
    // 关闭套接字的读功能,不再接收数据
    if (shutdown(sockfd, SHUT_RD) == -1) {
        perror("shutdown");
        return 1;
    }

    // 关闭套接字
    if (close(sockfd) == -1) {
        perror("close");
        return 1;
    }

    return 0;
}

在这个示例中,首先通过 socket 函数创建了一个套接字,然后与服务器建立连接。随后,调用 shutdown 函数分别关闭了套接字的写和读功能,最后关闭了套接字。

TCP回射服务器程序 👀
#include "unp.h"

int main(int argc, char **argv) {
    int i, maxi, maxfd, listenfd, connfd, sockfd;
    int nready, client[FD_SETSIZE];
    ssize_t n;
    fd_set rset, allset;
    char line[MAXLINE];
    char line_new[MAXLINE];//my code
    int j;//
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    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(SERV_PORT);

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);
    maxfd = listenfd;

    maxi = -1;//client的最大值
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1; //初始化客户数组
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    for ( ; ; ) {
        rset = allset;
        nready = Select(maxfd+1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(listenfd, &rset)) {
            clilen = sizeof(cliaddr);
            connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
            for (i = 0; i < FD_SETSIZE; i++)
                if (client[i] < 0) {
                    client[i] = connfd; /* save descriptor */
                    break;
                }
            if (i == FD_SETSIZE)
                err_quit("too many clients");
            FD_SET(connfd, &allset); /* add new descriptor to set */
            if (connfd > maxfd)
                maxfd = connfd; /* for select */
            if (i > maxi)
                maxi = i; /* max index in client[] array */
            if (--nready <= 0)
                continue; /* no more readable descriptors */
        }
        for (i = 0; i <= maxi; i++) {
            if ( (sockfd = client[i]) < 0)
                continue;
            if (FD_ISSET(sockfd, &rset)) {
                if ( (n = Readline(sockfd, line, MAXLINE)) == 0) {
                    Close(sockfd);
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
               }
            else{
                //my code
                j = 0;
                while(line[j] != '\0'){
                    if (line[j] >= 'A' && line[j] <= 'Z'){
                        line_new[j] = line[j] -'A' + 'a';
                    }
                    else if(line[j] >= 'a' && line[j] <= 'z'){
                        line_new[j] = line[j] - 'a' + 'A';
                    }
                    else
                        line_new[j] = line[j];
                    j++;
                }
                writen(sockfd, line_new, n);
            }
            if (--nready <= 0)
                break;
            }
            /*这段代码中的 `if (--nready <= 0) break;` 语句的作用是在处理完所有可读取的套接字后,检查是否还有剩余的可读取套接字。如果没有剩余的可读取套接字,那么 `nready` 的值将会减小到 0 或者小于等于 0。这时候,`nready` 的值表示没有更多的可读取套接字了,因此程序就可以继续等待新的连接或者已连接套接字上的数据到达,而无需再次进入 `for` 循环的迭代过程。
            */
        }
    }
}

TCP回射客户端程序 👀
main函数类似ch1中
#include "unp.h"
void
str_cli(FILE *fp,int sockfd)
{
	int maxfdp1,stdineof;
	fd_set rset;
	char sendline[MAXLINE], recvline[MAXLINE];
	
	stdineof = 0; //标志未0表示未读完
	FD_ZERO(&rset);
	for( ; ; ){
	    if (stdineof ==0)
	        FD_SET(fileno(fp), &rset);
	    FD_SET(sockfd, &rset);
	    maxfdp1 = max(fileno(fp), sockfd)+1;
	    Select(maxfdp1,&rset,NULL,NULL,NULL);
	
	    if (FD_ISSET(sockfd,&rset)){
	        if (Readline(sockfd,recvline,MAXLINE)==0){
	            if (stdineof ==1)
	                return;
	            else
	                err_quit("str_cli:server terminated prematurely");
	        }
	        Fputs(recvline,stdout);
	    }
	    if (FD_ISSET(fileno(fp), &rset)){
	        if (Fgets(sendline,MAXLINE,fp)==NULL){
	            stdineof = 1;
	            Shutdown(sockfd,SHUT_WR);
	            FD_CLR(fileno(fp),&rset);
	            continue;
	        }
			Writen(sockfd, sendline, strlen(sendline));
	    }
	}
}

pselect函数
poll函数
struct pollfd{
	int fd;
	int event;
	int revent;//result event
}
int poll(struct pollfd* fdarray, usigned long nfds, int timeout);

返回值:准备好的套接字的个数,0—超时 -1—出错

epoll函数

使用一组函数,而不是一个。
将人们关心的文件字符集,存放一个事件表中。不必像select和poll每次调用 都要重复输入文件描述符集

int epoll_create(int size); 创建内核事件表

int epoll_ctl() 操作内核事件表 (ctl – control)
#include <syslepoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);`

epfd : epoll_create()的返回值,事件表的标志码。
op: 操作
EPOLL_CTL_ADD:往事件表上注册fd上的事件
EPOLL_CTL_MOD:修改fd上的事件
EPOLL_CTL_DEL:删除fd上的事件
fd : 文件描述符
event参数指定事件。
成功:返回0,-1出错并设置errno

epoll_wait

#include <syslepoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents,int timeout);

epoll系列系统调用的主要接口,它在一段时间内等待一组文件描述符上的事件。
timeout参数的含义与poll的timeout参数相同, maxevents参数指定最多监听多少个事件,它必须大于0。
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中。
成功:返回就绪的文件描述符的个数,-1出错并设置errno

ch7 【不考】

ch8 基本UDP套接口编程

概述

udp概述

recvfrom和sendto函数

recvfromsendto 是用于在网络编程中进行数据收发的函数,通常在UDP(用户数据报协议)套接字编程中使用。

recvfrom

recvfrom: 这个函数用于从一个套接字接收数据,并且可以指定发送数据的源地址。
recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)sockfd 是接收数据的套接字描述符,buf 是一个指向接收数据缓冲区的指针,len 是缓冲区的大小,flags 是额外的标志,一般为0,src_addr 是一个指向存放发送数据源地址的结构体的指针,addrlensrc_addr 结构体的大小。

sendto

sendto: 这个函数用于将数据发送到指定的地址,可以指定目标地址。
它的原型通常是 sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)sockfd 是发送数据的套接字描述符,buf 是一个指向待发送数据的指针,len 是待发送数据的大小,flags 是额外的标志,一般为0,dest_addr 是一个指向目标地址结构体的指针,addrlendest_addr 结构体的大小。

这两个函数允许程序员在UDP通信中指定特定的发送和接收地址,而不像 recvsend 函数那样,只能在已经通过 bind 函数绑定了的套接字上进行数据收发。

UDP回射服务器程序
#include	"unp.h"

int
main(int argc, char **argv)
{
	int					sockfd;
	struct sockaddr_in	servaddr, cliaddr;

	sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
	//udp编程使用SOCK_DGRAM

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);

	Bind(sockfd, (SA *) &servaddr, sizeof(servaddr));

	dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));
}

dg_echo

#include	"unp.h"

void
dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
//pcliaddr -- point client address

{
	int			n;
	socklen_t	len;
	char		mesg[MAXLINE];

	for ( ; ; ) {
		len = clilen;
		n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
		//从sockfd接收数据,存储到msg(大小MAXLINE)中 这里第4个设置为0表示没有特殊的标志。
		/*
		ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
		flags 是用于控制接收行为的参数,通常用于设置接收数据的标志,例如 MSG_DONTWAIT 可以使 recvfrom 函数变为非阻塞模式。
		src_addr 是一个指向存放发送方地址信息的结构体的指针,用于存储发送方的 IP 地址和端口号。
		addrlen 是一个指向存放发送方地址信息长度的变量的指针,用于传递发送方地址信息的长度。
		*/
	
		Sendto(sockfd, mesg, n, 0, pcliaddr, len);
	}
}

UDP回射客户程序
#include	"unp.h"

int
main(int argc, char **argv)
{
	int					sockfd;
	struct sockaddr_in	servaddr;

	if (argc != 2)
		err_quit("usage: udpcli <IPaddress>");

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

	sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

	dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));

	exit(0);
}

dg_cli

#include	"unp.h"

void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int	n;
	char	sendline[MAXLINE], recvline[MAXLINE + 1];

	while (Fgets(sendline, MAXLINE, fp) != NULL) {
//char *fgets(char *str, int size, FILE *stream);
/*
用于从指定的流中读取一行数据
*/
		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

		recvline[n] = 0;	/* null terminate */
		Fputs(recvline, stdout);
	}
}

为什么客户端是先sendto后recvfrom

在客户端先执行 sendto 函数然后再执行 recvfrom 函数的原因主要是与 UDP 协议的特性以及典型的客户端-服务器通信模式有关。

UDP 是一种无连接的传输协议,它不会像 TCP 那样在通信开始时建立连接,而是直接发送数据包。因此,在使用 UDP 进行通信时,通常不需要在客户端和服务器之间建立显式的连接。

客户端先执行 sendto 函数,是因为客户端通常是主动发送数据的一方。通过 sendto 函数,客户端将数据包发送给服务器,而不需要等待服务器的响应。

然后客户端执行 recvfrom 函数,来等待服务器的响应。在典型的客户端-服务器通信模式中,客户端发送请求到服务器,然后等待服务器对请求进行处理并返回响应。因此,客户端需要在发送数据后等待服务器的响应,以便获取服务器的处理结果或者其他信息。

因此,先发送后接收是一种常见的客户端行为模式,它符合 UDP 协议的特性以及典型的客户端-服务器通信模式。

程序运行异常处理 ⬇

数据报的丢失

验证收到的响应

服务器进程未进行

UDP的connect函数

与tcp不同,没有三路握手过程。内核只是记录对方的IP地址和端口号。

未连接的udp套接口(缺省/默认情况) --> 已连接的udp套接口

已连接udp相较于未连接的变化

  1. 不能给输出操作指定目的IP地址和端口号,也就是不使用sendto而是使用write或者send
  2. 不使用recvfrom而是使用read或recv
  3. 异步错误由已连接udp套接口返回给进程

udp接口多次调用connect 目的

  1. 指定新的ip地址和端口号
  2. 断开套接口

使用connect的udp客户端程序

#include    "unp.h"

void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
    int     n;
    char    sendline[MAXLINE], recvline[MAXLINE + 1];
    Connect(sockfd, (SA *) pservaddr, servlen);
    while (Fgets(sendline, MAXLINE, fp) != NULL) {
        Write(sockfd, sendline, strlen(sendline));
        n = Read(sockfd, recvline, MAXLINE);
        recvline[n] = 0;    /* null terminate */
        Fputs(recvline, stdout);
    }
}
UDP的其它问题
  1. udp缺乏流量控制(没调用bind函数)
  2. udp接口接收缓冲区
  3. udp中外出接口的确定
使用select函数的TCP和UDP回射服务器程序

原则:大部分tcp是并发(fork) 大部分udp是迭代

/* include udpservselect01 */
#include	"unp.h"

int
main(int argc, char **argv)
{
	int					listenfd, connfd, udpfd, nready, maxfdp1; //第一步创建描述字集合
	
	char				mesg[MAXLINE];
	pid_t				childpid;
	fd_set				rset;
	ssize_t				n;
	socklen_t			len;
	const int			on = 1;
	struct sockaddr_in	cliaddr, servaddr;
	void				sig_chld(int);

		/* 4create listening TCP socket */
	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(SERV_PORT);

	//设置套接口选项 防止此端口上已经有连接存在
	Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);

		/* 4create UDP socket */
	udpfd = Socket(AF_INET, SOCK_DGRAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(SERV_PORT);

	Bind(udpfd, (SA *) &servaddr, sizeof(servaddr));
/* end udpservselect01 */

/* include udpservselect02 */
	Signal(SIGCHLD, sig_chld);	/* must call waitpid() */

	FD_ZERO(&rset);
	maxfdp1 = max(listenfd, udpfd) + 1;
	for ( ; ; ) {
		FD_SET(listenfd, &rset);
		FD_SET(udpfd, &rset);
		if ( (nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {
			if (errno == EINTR)
				continue;		/* back to for() */
			else
				err_sys("select error");
		}

		if (FD_ISSET(listenfd, &rset)) {
			len = sizeof(cliaddr);
			connfd = Accept(listenfd, (SA *) &cliaddr, &len);
	
			if ( (childpid = Fork()) == 0) {	/* child process */
				Close(listenfd);	/* close listening socket */
				str_echo(connfd);	/* process the request */
				exit(0);
			}
			Close(connfd);			/* parent closes connected socket */
		}

		if (FD_ISSET(udpfd, &rset)) {
			len = sizeof(cliaddr);
			n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *) &cliaddr, &len);

			Sendto(udpfd, mesg, n, 0, (SA *) &cliaddr, len);
		}
	}
}
/* end udpservselect02 */

ch9 基本名字与地址转换

概述

地址和域名-----应该使用名字而不是数值,这有很多原因:名字比较容易记;数值地址可以改变但名字保持不变;随着往IPv6上转移,数值地址变得更长,手工键入一个地址更易出错。

域名系统

DNS Domain Name System

主机名 – 1全限定域名FQDN/绝对名字absolute name
2简单名字

CNAME-- canonical name 规范名字

gethostbyname函数

gethostbyname函数是一个网络编程中常用的函数,它可以通过域名获取IP地址。在C语言中,该函数的原型为:

struct hostent *gethostbyname(const char *name);

其中,参数name为要查询的主机名或IP地址字符串,函数返回一个指向hostent结构体的指针。

hostent结构体包含了主机的相关信息,如主机名、别名、地址类型和地址等。可以通过该结构体来获取主机的IP地址

例如
struct hostent* hostinfo = gethostbyname("www.baidu.com");

gethostbyname2函数与IPv6支持

gethostname2(host, AF_INET)
gethostname2(host, AF_INET6)

gethostbyaddr函数

gethostbyaddr 函数用于通过 IP 地址获取主机的相关信息,例如主机名、别名和地址类型等。它是在网络编程中使用的一个函数,常用于根据 IP 地址来获取主机名。

以下是 gethostbyaddr 函数的基本信息:

  • 原型:struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
  • 参数:
    • addr:指向包含 IP 地址的结构体的指针。
    • len:指定地址结构体的长度。
    • type:指定地址类型,通常设置为 AF_INET 表示 IPv4 地址族。
  • 返回值:如果成功,则返回指向 hostent 结构体的指针,该结构体包含了主机的相关信息。如果出错,则返回 NULL。

hostent 结构体包含了主机的信息,例如主机名、别名和地址类型等。其定义通常如下:

struct hostent {
    char  *h_name;            // 官方主机名
    char **h_aliases;         // 主机别名列表
    int    h_addrtype;        // 地址类型
    int    h_length;          // 地址长度
    char **h_addr_list;       // 主机地址列表
};
  • h_name:官方主机名。
  • h_aliases:主机别名列表。
  • h_addrtype:地址类型,通常为 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • h_length:地址长度。
  • h_addr_list:主机地址列表,一个以 NULL 结尾的字符串数组,每个元素是一个地址。

使用 gethostbyaddr 函数可以根据给定的 IP 地址获取主机的相关信息,例如:

#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>

int main() {
    struct in_addr addr;
    struct hostent *host;

    // 将 IP 地址转换为网络字节序
    inet_aton("192.0.2.1", &addr);

    // 获取主机信息
    host = gethostbyaddr(&addr, sizeof(addr), AF_INET);
    if (host == NULL) {
        perror("gethostbyaddr");
        return 1;
    }

    printf("Official Host Name: %s\n", host->h_name);
    printf("Aliases:\n");
    for (char **alias = host->h_aliases; *alias != NULL; alias++) {
        printf("  %s\n", *alias);
    }

    return 0;
}

此示例将 IP 地址 192.0.2.1 转换为主机信息,并打印出官方主机名和别名列表。

uname和gethostname函数

unamegethostname 是两个用于获取主机信息的函数,它们在操作系统编程中常被用到。

  1. uname 函数
    • uname 函数用于获取系统信息,包括操作系统名称、主机名、内核版本等。
    • 原型:int uname(struct utsname *buf);
    • 参数:
      • buf:指向一个 utsname 结构体的指针,用于保存系统信息。
    • 返回值:若成功则返回 0,若出错则返回 -1。

utsname 结构体的定义如下:

struct utsname {
    char sysname[65];   // 操作系统名称
    char nodename[65];  // 主机名
    char release[65];   // 内核版本
    char version[65];   // 操作系统版本
    char machine[65];   // 硬件类型
};

示例代码:

#include <stdio.h>
#include <sys/utsname.h>

int main() {
    struct utsname info;
    if (uname(&info) != 0) {
        perror("uname");
        return 1;
    }

    printf("Operating System: %s\n", info.sysname);
    printf("Hostname: %s\n", info.nodename);
    printf("Kernel Version: %s\n", info.release);
    printf("OS Version: %s\n", info.version);
    printf("Machine: %s\n", info.machine);

    return 0;
}
  1. gethostname 函数
    • gethostname 函数用于获取主机名。
    • 原型:int gethostname(char *name, size_t len);
    • 参数:
      • name:指向用于存储主机名的缓冲区。
      • len:缓冲区的大小。
    • 返回值:若成功则返回 0,若出错则返回 -1。

示例代码:

#include <stdio.h>
#include <unistd.h>

int main() {
    char hostname[256];
    if (gethostname(hostname, sizeof(hostname)) != 0) {
        perror("gethostname");
        return 1;
    }

    printf("Hostname: %s\n", hostname);

    return 0;
}

这两个函数在系统编程中常用于获取系统和主机的信息,用于识别系统特征、生成日志或进行网络通信等。

getservbyname和getservbyport函数

getservbynamegetservbyport 是两个用于获取网络服务信息的函数,它们在网络编程中常被用到。

  1. getservbyname 函数
    • getservbyname 函数通过服务名获取服务的信息,包括端口号和协议类型。
    • 原型:struct servent *getservbyname(const char *name, const char *proto);
    • 参数:
      • name:要查询的服务名。
      • proto:要查询的协议类型,通常为 "tcp""udp"
    • 返回值:若成功则返回指向 servent 结构体的指针,若出错则返回 NULL。

servent 结构体的定义如下:

struct servent {
    char  *s_name;       // 官方服务名
    char **s_aliases;    // 服务别名列表
    int    s_port;       // 端口号
    char  *s_proto;      // 协议名
};

示例代码:

#include <stdio.h>
#include <netdb.h>

int main() {
    struct servent *serv;
    serv = getservbyname("http", "tcp");
    if (serv == NULL) {
        perror("getservbyname");
        return 1;
    }

    printf("Service Name: %s\n", serv->s_name);
    printf("Port Number: %d\n", ntohs(serv->s_port)); // 注意将端口号从网络字节序转换为主机字节序
    printf("Protocol: %s\n", serv->s_proto);
    return 0;
}
  1. getservbyport 函数
    • getservbyport 函数通过端口号和协议类型获取服务的信息,包括服务名和协议类型。
    • 原型:struct servent *getservbyport(int port, const char *proto);
    • 参数:
      • port:要查询的端口号,需以网络字节序表示。
      • proto:要查询的协议类型,通常为 "tcp""udp"
    • 返回值:若成功则返回指向 servent 结构体的指针,若出错则返回 NULL。

示例代码:

#include <stdio.h>
#include <netdb.h>

int main() {
    struct servent *serv;
    serv = getservbyport(htons(80), "tcp"); // 注意将端口号从主机字节序转换为网络字节序
    if (serv == NULL) {
        perror("getservbyport");
        return 1;
    }

    printf("Service Name: %s\n", serv->s_name);
    printf("Port Number: %d\n", ntohs(serv->s_port)); // 注意将端口号从网络字节序转换为主机字节序
    printf("Protocol: %s\n", serv->s_proto);
    return 0;
}

这两个函数用于获取服务的信息,可以根据服务名或者端口号来查询对应的服务信息,常用于网络编程中的服务发现和端口映射等场景。

  • 33
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LagomIsBest

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值