《TCP/IP网络编程》读书笔记
阅读原文时间:2021年08月26日阅读:1

1.Windows 下的 socket 程序和 Linux 思路相同,但细节有所差别
(1) Windows 下的 socket 程序依赖 Winsock.dll 或 ws2_32.dll,必须提前加载。DLL 有两种加载方式,请查看:动态链接库DLL的加载
(2) Linux 使用“文件描述符”的概念,而 Windows 使用“文件句柄”的概念;Linux 不区分 socket 文件和普通文件,而 Windows 区分;
(3) Linux 下 socket() 函数的返回值为 int 类型,而 Windows 下为 SOCKET 类型,也就是句柄。
(4) Linux 下使用 read() / write() 函数读写,而 Windows 下使用 recv() / send() 函数发送和接收。
(5) 关闭 socket 时,Linux 使用 close() 函数,而 Windows 使用 closesocket() 函数。

2.分配给标准输入标准输出及标准错误输出的文件描述符
文件描述符       对象
 0                  标准输出
 1                  标准输出
 2                  标准错误

3.WinSock(Windows Socket)编程依赖于系统提供的动态链接库(DLL),有两个版本:

较早的DLL是 wsock32.dll,大小为 28KB,对应的头文件为 winsock1.h;
最新的DLL是 ws2_32.dll,大小为 69KB,对应的头文件为 winsock2.h。
使用#pragma命令,在编译时加载:
 #pragma comment (lib, "ws2_32.lib")
WinSock 编程的第一步就是加载 ws2_32.dll,然后调用 WSAStartup() 函数进行初始化,并指明要使用的版本号

WSADATA wsaData;
WSAStartup( MAKEWORD(, ), &wsaData);

4. socket编程

(1) 使用socket()函数创建套接字
(2) 使用bind()函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理
(3) 使用connect()函数来建立连接

(4) sockaddr结构体
可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体
另外还有 sockaddr_in6,用来保存 IPv6 地址

(5) 使用listen()函数让套接字进入被动监听状态

(6) 使用accept()函数可以随时响应客户端的请求
(7) accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号
(8) listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到accept()
(9) accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。
(10) TCP服务器端函数调用顺序:socket()->bind()->listen()->accept()->read()/write()->close()
(11) TCP客户端函数调用顺序:socket()->connect()->read()/write()->close()

例子:

(1)基于linux环境的tcp回声服务器的实现

#include
#include
#include
#include
#include
#include

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;

 struct sockaddr\_in serv\_adr;  
 struct sockaddr\_in clnt\_adr;  
 socklen\_t clnt\_adr\_sz;

 if(argc!=) {  
     printf("Usage : %s <port>\\n", argv\[\]);  
     exit();  
 }

 serv\_sock=socket(PF\_INET, SOCK\_STREAM, );  
 if(serv\_sock==-)  
     error\_handling("socket() error");

 memset(&serv\_adr, , sizeof(serv\_adr));  
 serv\_adr.sin\_family=AF\_INET;  
 serv\_adr.sin\_addr.s\_addr=htonl(INADDR\_ANY);  
 serv\_adr.sin\_port=htons(atoi(argv\[\]));

 if(bind(serv\_sock, (struct sockaddr\*)&serv\_adr, sizeof(serv\_adr))==-)  
     error\_handling("bind() error");

 if(listen(serv\_sock, )==-)  
     error\_handling("listen() error");

 clnt\_adr\_sz=sizeof(clnt\_adr);

 for(i=; i<; i++)  
 {  
     clnt\_sock=accept(serv\_sock, (struct sockaddr\*)&clnt\_adr, &clnt\_adr\_sz);  
     if(clnt\_sock==-)  
         error\_handling("accept() error");  
     else  
         printf("Connected client %d \\n", i+);

     while((str\_len=read(clnt\_sock, message, BUF\_SIZE))!=)  
         write(clnt\_sock, message, str\_len);

     close(clnt\_sock);  
 }

 close(serv\_sock);  
 return ;  

}

void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

(2)基于linux环境的tcp回声客户端的实现

#include
#include
#include
#include
#include
#include

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;

 if(argc!=) {  
     printf("Usage : %s <IP> <port>\\n", argv\[\]);  
     exit();  
 }

 sock=socket(PF\_INET, SOCK\_STREAM, );  
 if(sock==-)  
     error\_handling("socket() error");

 memset(&serv\_adr, , sizeof(serv\_adr));  
 serv\_adr.sin\_family=AF\_INET;  
 serv\_adr.sin\_addr.s\_addr=inet\_addr(argv\[\]);  
 serv\_adr.sin\_port=htons(atoi(argv\[\]));

 if(connect(sock, (struct sockaddr\*)&serv\_adr, sizeof(serv\_adr))==-)  
     error\_handling("connect() error!");  
 else  
     puts("Connected...........");

 while()  
 {  
     fputs("Input message(Q to quit): ", stdout);  
     fgets(message, BUF\_SIZE, stdin);

     if(!strcmp(message,"q\\n") || !strcmp(message,"Q\\n"))  
         break;

     write(sock, message, strlen(message));  
     str\_len=read(sock, message, BUF\_SIZE-);  
     message\[str\_len\]=;  
     printf("Message from server: %s", message);  
 }

 close(sock);  
 return ;  

}

void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

5.Ack号 = Seq号 + 传递的字节数 + 1

6.

 (1) 调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据
 (2) 使用 shutdown() 函数可以只断开一条数据传输通道,而保留另一条

int shutdown(int sock, int howto); //Linux
int shutdown(SOCKET s, int howto); //Windows

sock 为需要断开的套接字,howto 为断开方式

howto 在 Linux 下有以下取值:

  • SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
  • SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
  • SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。

howto 在 Windows 下有以下取值:

  • SD_RECEIVE:关闭接收操作,也就是断开输入流。
  • SD_SEND:关闭发送操作,也就是断开输出流。
  • SD_BOTH:同时关闭接收和发送操作。

shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了
 (3) 默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包, 也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

例子:

(1)file_server.c

#include
#include
#include
#include
#include
#include

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
int serv_sd, clnt_sd;
FILE * fp;
char buf[BUF_SIZE];
int read_cnt;

 struct sockaddr\_in serv\_adr, clnt\_adr;  
 socklen\_t clnt\_adr\_sz;

 if(argc!=) {  
     printf("Usage: %s <port>\\n", argv\[\]);  
     exit();  
 }

 fp=fopen("file\_server.c", "rb");  
 serv\_sd=socket(PF\_INET, SOCK\_STREAM, );   

 memset(&serv\_adr, , sizeof(serv\_adr));  
 serv\_adr.sin\_family=AF\_INET;  
 serv\_adr.sin\_addr.s\_addr=htonl(INADDR\_ANY);  
 serv\_adr.sin\_port=htons(atoi(argv\[\]));

 bind(serv\_sd, (struct sockaddr\*)&serv\_adr, sizeof(serv\_adr));  
 listen(serv\_sd, );

 clnt\_adr\_sz=sizeof(clnt\_adr);  
 clnt\_sd=accept(serv\_sd, (struct sockaddr\*)&clnt\_adr, &clnt\_adr\_sz);

 while()  
 {  
     read\_cnt=fread((void\*)buf, , BUF\_SIZE, fp);  
     if(read\_cnt<BUF\_SIZE)  
     {  
         write(clnt\_sd, buf, read\_cnt);  
         break;  
     }  
     write(clnt\_sd, buf, BUF\_SIZE);  
 }

 shutdown(clnt\_sd, SHUT\_WR);  
 read(clnt\_sd, buf, BUF\_SIZE);  
 printf("Message from client: %s \\n", buf);

 fclose(fp);  
 close(clnt\_sd); close(serv\_sd);  
 return ;  

}

void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

(2)file_client.c

#include
#include
#include
#include
#include
#include

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
int sd;
FILE *fp;

 char buf\[BUF\_SIZE\];  
 int read\_cnt;  
 struct sockaddr\_in serv\_adr;  
 if(argc!=) {  
     printf("Usage: %s <IP> <port>\\n", argv\[\]);  
     exit();  
 }

 fp=fopen("receive.dat", "wb");  
 sd=socket(PF\_INET, SOCK\_STREAM, );   

 memset(&serv\_adr, , sizeof(serv\_adr));  
 serv\_adr.sin\_family=AF\_INET;  
 serv\_adr.sin\_addr.s\_addr=inet\_addr(argv\[\]);  
 serv\_adr.sin\_port=htons(atoi(argv\[\]));

 connect(sd, (struct sockaddr\*)&serv\_adr, sizeof(serv\_adr));

 while((read\_cnt=read(sd, buf, BUF\_SIZE ))!=)  
     fwrite((void\*)buf, , read\_cnt, fp);

 puts("Received file data");  
 write(sd, "Thank you", );  
 fclose(fp);  
 close(sd);  
 return ;  

}

void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

**7. recv() 返回 0 的唯一时机就是收到FIN包时
**

8. 网络字节序
不同 CPU 保存和解析数据的方式不同(主流的 Intel 系列 CPU 为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。

9.
ping 域名可以查看域名对应的IP地址
nslookup命令可以查看计算机中注册的默认DNS服务器地址

10.
 (1) 下列函数可以通过传递字符串格式的域名获取IP地址

#include
struct hostent *gethostbyname(const char *hostname);

成功时返回hostnet结构体指针,失败时返回NULL指针

例子:gethostbyname.c

#include
#include
#include
#include
#include
void error_handling(char *message);

int main(int argc, char *argv[])
{
int i;
struct hostent *host;
if(argc!=) {
printf("Usage : %s \n", argv[]);
exit();
}

 host=gethostbyname(argv\[\]);  
 if(!host)  
     error\_handling("gethost... error");

 printf("Official name: %s \\n", host->h\_name);

 for(i=; host->h\_aliases\[i\]; i++)  
     printf("Aliases %d: %s \\n", i+, host->h\_aliases\[i\]);

 printf("Address type: %s \\n",  
     (host->h\_addrtype==AF\_INET)?"AF\_INET":"AF\_INET6");

 for(i=; host->h\_addr\_list\[i\]; i++)  
     printf("IP addr %d: %s \\n", i+,  
                 inet\_ntoa(\*(struct in\_addr\*)host->h\_addr\_list\[i\]));  
 return ;  

}

void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

struct hostnet
{
char *h_name;
char **h_aliases;
int h_addrtype;
int h_lenght;
char **h_addr_list;
}

(2) gethostbyaddr()函数利用IP地址获取域相关信息

#include
struct hostnet *gethostbyaddr(const char *addr, socklen_t len, int fanily);

成功时返回hostnet结构体变量地址值,失败时返回NULL指针

addr:含有IP地址信息的in_addr结构体指针
 len:  向第一个参数传递的地址信息的字节数,IPv4时为4,IPv6时为6
 family: 传递地址族信息,IPv4时为AF_INET, IPv6时为AF_INET6

例子:gethostbyaddr.c

#include
#include
#include
#include
#include
#include
void error_handling(char *message);

int main(int argc, char *argv[])
{
int i;
struct hostent *host;
struct sockaddr_in addr;
if(argc!=) {
printf("Usage : %s \n", argv[]);
exit();
}

 memset(&addr, , sizeof(addr));  
 addr.sin\_addr.s\_addr=inet\_addr(argv\[\]);  
 host=gethostbyaddr((char\*)&addr.sin\_addr, , AF\_INET);  
 if(!host)  
     error\_handling("gethost... error");

 printf("Official name: %s \\n", host->h\_name);

 for(i=; host->h\_aliases\[i\]; i++)  
     printf("Aliases %d: %s \\n", i+, host->h\_aliases\[i\]);

 printf("Address type: %s \\n",  
     (host->h\_addrtype==AF\_INET)?"AF\_INET":"AF\_INET6");

 for(i=; host->h\_addr\_list\[i\]; i++)  
     printf("IP addr %d: %s \\n", i+,  
                 inet\_ntoa(\*(struct in\_addr\*)host->h\_addr\_list\[i\]));  
 return ;  

}

void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit();
}

(3)字节序转换函数

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

(4) 将字符串信息转换为网络字节序的整数型

#include
in_addr_t inet_addr(const char *string);

成功时返回32位大端序整数型值,失败时返回INADDR_NONE

(5) inet_aton()也将字符串形式IP地址转换位32位网络字节序整数并返回,只不过该函数利用in_addr结构体,且其使用频率跟高

#include
int inet_aton(const char *string, struct in_addr *addr);

成功时返回1(true),失败时返回0(false)

(6) inet_ntoa()函数可以把网络字节序整数型IP地址转换成字符串形式

#include
char *inet_ntoa(struct in_addr adr);

成功时返回转换的字符串地址值,失败时返回-1;

调用完该函数后应立即将字符串信息复制到其他内存空间,因为,若再次调用inet_ntoa函数,则有可能覆盖之前保存的字符串信息

 (7) 网络地址信息初始化方法:

struct sockaddr_in addr;
char *serv_ip = "211.217.168.13";
char *serv_port "";
memset(&addr, , sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(serv_ip);
addr.sin_port = htons(atoi(serv_port));

利用常数INADDR_ANY分配服务器的IP地址,可以自动获取运行服务器端的计算机IP地址

addr.sin_addr.s_addr = htonl(INADDR_ANY);

11.套接字的可选项

getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen)

  • SO_SNDBUF:输入缓冲大小相关可选项

  • SO_RCVBUF:输出缓冲大小相关可选项

  • SO_REUSEADDR:该可选项设置为TRUE可将Time_wait状态下的套接字端口号重新分配给新的套接字

TCP_NODELAY设置为1可禁用Nagle算法

12.多进程

(1) 通过调用fork函数创建进程

#include
pid_t fork(void);

父进程:fork函数返回子进程的ID

子进程:fork函数返回0

(2) 应当向创建子进程的父进程传递子进程的exit参数值或return语句的返回值,防止僵尸进程的产生
 销毁僵尸进程1:利用wait函数

#include
pid_t wait(int *statloc);

->成功时返回终止的子进程ID,失败时返回-1

子进程终止时传递的返回值将保存到statloc所指内存空间,需要用下列宏进行分离

  • WIFEXITED 子进程正常终止时返回“真”true
  • WEXITSTATUS 返回子进程的返回值

销毁僵尸进程2:利用waitpid函数

#include
pid_t waitpid(pid_t pid, int *statloc, int options);

->成功时返回终止的子进程ID,失败时返回-1

pid 等待终止的目标子进程ID,若传递-1,则可以等待任意子进程终止

 (3) statloc与wait函数的statloc参数具有相同含义
 (4) options 传递sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入组赛状态,而是返回0并退出函数
 (5)信号与signal函数

#include
void (*signal(int signal, void (*func)(void)))(int);

->为了在产生信号时调用,返回之前注册的函数指针

(6) 利用sigaction函数进行信号处理

#include
int sugacyion(int signo, const struct sigaction *act, struct sigaction *oldact);

->成功时返回0,失败时返回-1

通过fork函数复制套接字文件描述符后,同一端口将对应多个套接字,只有这些套接字描述符都终止,才能销毁套接字
 
13.进程间通信
 创建管道的函数:

#include
int pipe(int filedes[]);

->成功时返回0,失败时返回-1

filedes[0]:通过管道接收数据时使用的文件描述符,即管道出口
 filedse[1]:通过管道传输数据时使用的文件描述符,即管道入口
 
14.I/O复用
 (1) 针对fd_set变量的操着的宏:

FD_ZERO(fd_set *fdset)
FD_SET(int fd, fd_set *fdset)
FD_CLR(int fd, fd_set *fdset)
FD_ISSET(fint fd, d_set *fdset)

(2) select函数:

#include
#include
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

15.多种I/O函数

(1) 收到MSG_OOB紧急消息时,操着系统将产生SIGURG消息,并调用注册的信号处理函数
 (2) 处理SIGURG信号时必须指定处理信号的进程,而geipid函数返回调用此函数的进程ID

 (3) fcntl函数用于控制文件描述符

fcntl(recv_sock, F_SETOWN, getpid());

上述调用的含义是“将文件描述符recv_sock指向的套接字拥有者(F_SETOWN)改为把getpid函数返回值用作ID的进程

(4) 紧急指针指向紧急消息的下一个位置(偏移量+1),紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息
 (5) 调用recv函数的同时传递MSG_PEEK可选项,是为了保证即使不存在待读取的数据也不会进入阻塞状态,设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除,该选项通常与MSG_DONTWAIT合作,用于调用以非阻塞方式验证待读取数据存在与否的函数

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章