推荐书籍 TCP/IP网络编程,是目前我读的最适合我的网络编程书籍。
此系列博客是我学习这本书的记录,算是代替纸张笔记了。
第一章:网络编程与套接字
1、1 概要:
-
网络编程:编写程序使两台联网的计算机相互交换数据。
-
套接字:是网络数据传输用的软件设备。连接因特网的工具。
-
创建等待连接请求的服务器端。 服务器端套接字或监听套接字。
- 由socket函数生成套接字。
#include <sys/socket.h> // 调用 socket 函数创建套接字 int socket(int domain, int type, int protocol); // 成功时返回文件描述符,失败时返回-1.
- 给创建好的套接字分配地址信息(IP 地址 和端口号)
#include <sys/socket.h> //调用 bind 函数分配ip地址和端口号 int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); // 成功返回0,失败返回-1.
- 将套接字转化为可接收连接的状态,之后就可以收到连接请求。
#include <sys/socket.h> // 调用 listen 函数将套接字转为可接受连接状态 int listen(int sockfd, int backlog); // 成功返回0,失败返回-1.
- 接收对方的连接请求来完成数据传输。
#include <sys/socket.h> // accept函数受理连接请求,如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);// 成功时返回文件描述符,失败返回-1
- 等待连接请求的套接字创建过程总结:
- 第一步:调用socket 函数创建套接字。
- 第二步:调用bind函数分配 IP 地址和端口号。
- 第三步:调用listen函数转为可接收请求状态。
- 第四步:调用accept函数受理连接请求。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> void error_handling(char*message); int main(int argc,char *argv[]){ int serv_sock; int clnt_sock; // 定义在头文件 <arpa/inet.h>中,定义了Inernet socket address struct sockaddr_in serv_addr; struct sockaddr_in clnt_addr; socklen_t clnt_addr_size; char message[] = "Hello World!"; if(argc != 2){ printf("Usage : %s <port>\n",argv[0]); exit(1); } // PF_INET ,IP协议簇,#define PF_INET 2 // SOCK_STREAM 有序的、可信赖的、基于连接的字节流 // 调用socket函数生成 服务器socket套接字 serv_sock = socket(PF_INET,SOCK_STREAM,0); // socket函数成功返回文件描述符,失败返回-1 if(serv_sock == -1) error_handling("socket() error"); // sin_family 端口号,#define AF_INET PF_INET memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(atoi(argv[1])); // bind函数将创建好的套接字分配IP地址和端口号 if(bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr)) == -1) error_handling("listen() error"); // listen函数将套接字转化为可接受连接状态 if(listen(serv_sock,5) == -1) error_handling("listen() error"); clnt_addr_size = sizeof(clnt_addr); // accept函数受理连接请求,如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止 clnt_sock= accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_size); if (clnt_sock == -1) error_handling("accept() error"); //稍后要将介绍的 write 函数用于传输数据,若程序经过 accept 这一行执行到本行,则说明已经有了连接请求 write(clnt_sock, message, sizeof(message)); close(clnt_sock); close(serv_sock); return 0; } void error_handling(char *message) { // 将字符串写入 stderr 流中 fputs(message, stderr); // 将字符写入erroe stream中 fputc('\n', stderr); exit(1); }
请求连接的客户端套接字,(客户端套接字)
-
请求连接函数 connect,调用connect函数向服务器端发送请求。
-
#include <sys/socket.h> int connect(int sockfd,struct sockaddr * serv_addr, socklen_t addrlen); // 成功返回0,失败返回-1
-
请求连接的客户端套接字
- 调用socket函数创建套接字
- 调用connect函数向服务器端发送连接请求
-
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void error_handling(char *message); int main(int argc, char* argv[]){ int sock; struct sockaddr_in serv_addr; char message[30]; int str_len; if(argc!=3){ printf("Usage : %s <IP> <port>\n",argv[0]); exit(1); } // 调用 socket函数创建套接字 ,此时套接字不马上分为服务端和客户端,若调用bind、listen函数,称为服务端套接字,调用connect函数,是客户端套接字 sock = socket(PF_INET,SOCK_STREAM,0); if(sock == -1) error_handling("socket() error"); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); //调用 connect 函数向服务器发送连接请求 if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1) error_handling("connect() error"); str_len = read(sock,message,sizeof(message) -1); if(str_len == -1) error_handling("read() error"); printf("Message from server : %s \n", message); close(sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
-
编译运行:
-
服务器端需要在运行时接受客户端连接请求,所有先编译运行服务器端,运行后程序会停留在此。
-
gcc hello_server.c -o hserver ./hserver 9190
-
编译运行客户端
-
gcc hello_client.c -o hclient ./hclient 127.0.0.1 9190 // 127.0.0.1 是服务器端IP地址
-
1、2 基于Linux的文件操作
-
Linux系统中,socket也是文件的一种,所有网络数据传输中需要使用到文件I\O的相关函数。
-
底层文件访问(Low-Level File Access)和文件描述符(File Descriptor)
-
这里的底层指与标准无关的操作系统独立提供的。
-
文件描述符指系统分配给文件或套接字的整数。即操作系统创建文件或套接字赋予的数。又称为句柄(Windows平台),描述符(Linux平台)。
-
文件描述符 对象 0 标准输入:Standard Input 1 标准输出:standard Output 2 标准错误:Standard Error
-
-
打开文件函数:open
-
第一个参数是打开的目录文件名及路径信息,第二个参数是文件打开模式
-
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char * path, int flag);// 成功时返回文件描述符,失败返回-1
-
打开模式 含义 O_CREAT 必要时创建文件 O_TRUNC 删除全部现有数据 O_APPEND 维持现有数据,保存到其后面 O_RDONLY 只读打开 O_WRONLY 只写打开 O_RDWR 读写打开 -
打开模式如果需要传入多个参数,通过位或运算(OR)组合传递。
-
-
关闭文件函数:close
-
使用文件后要关闭。参数fd指要关闭的文件或套接字的文件描述符。
-
#include <unistd.h> int close(int fd); // 成功时返回0, 失败时返回-1
-
函数close不仅可以关闭文件还能关闭套接字。
-
-
将数据写入文件/套接字:write函数
-
向文件/套接字输出(传输)数据。
-
参数 fd 显示数据传输对象的文件描述符
-
buf 保存要传输数据的缓冲地址值
-
nbytes 要传输数据的字节数
-
// 标准符号常量和类型头文件,是C和C++的可移植操作系统API的头文件名称 #include <unistd.h> ssize_t write(int fd, const void * buf, size_t nbytes); // 成功时返回写入的字节数,失败返回-1
-
size_t 是 unsigned int 类型,ssize_t 是通过 typedef 声明的signed int 类型。
-
-
读取文件中的数据:read函数
-
read函数用来读取数据
-
#include <unistd.h> // 成功时返回接收的字节数(遇到文件结尾返回0),失败时返回-1 ssize_t read(int fd, void *buf, size_t nbytes);
-
fd显示数据接收对象的文件描述符
-
buf 要保存接收数据的缓冲地址值
-
nbytes 要接收数据的最大字节数。
-
-
-
描述符从3开始由以小到大的顺序编号,因为0、1、2是分配给标准I/O的描述符。
1、3 基于Windows平台的实现:
-
为Windows套接字编程设置头文件和库
- 开发网络程序的准备工作:
- 导入头文件winsock2.h
- 链接ws2_32.lib库
- 在项目属性中打开 “配置属性”-> “链接”-> “额外依赖”->“ws2_32.lib”;
- 开发网络程序的准备工作:
-
Winsock的初始化
-
进行Winsock编程时,需要调用WSAStartup函数,设置程序中用到的Winsock版本,初始化相应版本的库。
-
#include <winsock2.h> int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);// 成功时返回0,失败时返回非0的错误值代码
-
wVersionRequested 程序员要用的Winsock版本信息
-
Winsock存在多个版本,应准备WORD类型的套接字版本信息。(WORD是typedef 声明定义的unsigned short类型)并传递给WSAStartup函数的第一个参数wVersionRequested。
-
版本1.2则1是主版本号,2是副版本号,传递给wVersionRequest的值是0x0201, 高8位是副版本号,低8位是主版本号。
-
可以借助MAKEWORD宏函数构建WORD型版本信息。
-
MAKEWORD(1,2); // 主版本为1,副版本为2,返回0x0201 MAKEWORD(2,2); // 主版本为2,副版本为2,返回0x0202
-
-
lpWSAData : WSADATA结构体变量地址值
-
参数lpWSAData中需要传入WSADATA结构体变量地址。(LPWSADATA是WSADATA的指针类型)
-
调用完函数,相应参数中将添加已初始化的库信息,为了调用函数,必须传递WSADATA结构体变量地址。
-
int main(int argc, char * argv[]){ WSADATA wsaData; ...... if(WSAStartup(MAKEWORD(2,2),&wsaData)!= 0) ErrorHanding("WSAStartup() error!"); ...... return 0; }
-
-
-
Winsock相关库注销方法:WSACleanup
-
#include <winsock2.h> int WSACleanup(void); // 成功时返回0,失败时返回SOCKET_ERROR
-
调用WSACleanup函数后,Winsock库归还Windows系统,无法再调用Winsock相关函数,通常在程序结束前使用。
-
-
1、4 基于Windows的套接字相关函数
-
// 与linux socket函数提供相同功能。调用 socket 函数创建套接字 #include <winsock2.h> SOCKET socket(int af, int type, int protocol);// 成功时返回套接字句柄,失败返回INVALID_SOCKET
-
// 调用 bind 函数分配ip地址和端口号 #include <winsock2.h> int bind(SOCKET s, const struct sockaddr * name, int namelen);// 成功时返回0,失败返回SOCKET_ERROR
-
// listen函数将套接字转化为可接受连接状态,使其可接收客户端连接。 #include <winsock2.h> int listen(SOCKET s, int backlog);// 成功返回0,失败返回SOCKET_ERROR
-
//accept 函数受理连接请求,如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止 #include <winsock2.h> SOCKET accept(SOCKET s, struct sockaddr * addr, int *addrlen); // 成功返回套接字句柄,失败返回INVLID_SOCKET
-
// 调用 connect 函数向服务器发送连接请求 #include <winsock2.h> int connect(SOCKET s, const struct sockaddr *name,int namelen);//成功返回0,失败返回SOCKET_ERROR
-
// 关闭套接字 #include <winsock2.h> int closesocket(SOCKET s); // 成功返回0,失败返回 SOCKET_ERROR
-
Windows文件句柄与套接字句柄
- Windows句柄相当于Linux中的文件描述符,SOCKET类型是由typedef定义的保存套接字句柄整形值的数据类型。
-
Windows的服务端示例:
-
#include <stdio.h> #include <stdlib.h> #include <WinSock2.h> void ErrorHanding(const char* message); int main(int argc, char* argv[]) { WSADATA wsaData; SOCKET hServSock, hClntSock; SOCKADDR_IN servAddr, clntAddr; int szClntAddr; char message[] = "Hello World!"; if (argc != 2) { printf("Usage: %s <port>\n", argv[0]); exit(1); } // 成功时返回0,失败时返回非0的错误值代码,设置程序中用到的Winsock版本 // 初始化套接字库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) ErrorHanding("WSAStartup() error"); // 调用socket函数生成 服务器socket套接字, 成功时返回套接字句柄,失败返回INVALID_SOCKET // 创建套接字 hServSock = socket(PF_INET, SOCK_STREAM, 0); if (hServSock == INVALID_SOCKET) ErrorHanding("socket() error"); memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.s_addr = htonl(INADDR_ANY); servAddr.sin_port = htons(atoi(argv[1])); // 调用 bind 函数分配ip地址和端口号 if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) ErrorHanding("bind() error"); // listen函数将套接字转化为可接受连接状态,使其可接收客户端连接。 // 称为服务器端套接字 if (listen(hServSock, 5) == SOCKET_ERROR) ErrorHanding("Listen() error"); szClntAddr = sizeof(clntAddr); // accept函数受理连接请求,如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止 // accept 函数受理客户端请求 hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr); if (hClntSock == INVALID_SOCKET) ErrorHanding("accept() error"); // send()函数向客户端传输数据 send(hClntSock, message, sizeof(message), 0); closesocket(hClntSock); closesocket(hServSock); // 程序终止前注销套接字库 WSACleanup(); return 0; } void ErrorHanding(const char* message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
-
-
基于Windows端的客户端示例:
-
#include <stdio.h> #include <stdlib.h> #include <WinSock2.h> #pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll void ErrorHanding(const char* message); int main(int argc, char* argv[]) { WSADATA wsaData; SOCKET hSocket; SOCKADDR_IN servAddr; char message[30]; int strlen; if (argc != 3) { printf("Usage: %s <IP> <port>\n", argv[0]); exit(1); } // 初始化Winsock库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) ErrorHanding("WSAStartup() error!"); // 创建套接字 hSocket = socket(PF_INET, SOCK_STREAM, 0); if (hSocket == INVALID_SOCKET) ErrorHanding("socket() error"); memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.s_addr = inet_addr(argv[1]); servAddr.sin_port = htons(atoi(argv[2])); // 向服务端发送连接请求 if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) ErrorHanding("connect() error"); // recv函数接收服务器端发送来的数据 strlen = recv(hSocket, message, sizeof(message) - 1, 0); if (strlen == -1) ErrorHanding("read() error"); printf("Message from serve : %s \n", message); closesocket(hSocket); WSACleanup(); return 0; } void ErrorHanding(const char* message) { fputs("message", stderr); fputc('\n', stderr); exit(1); }
-
编译运行hello_server_win.cpp
-
gcc hello_server_win.cpp -o hServerWin -lws2_32 // 需要添加-lws2_32
-
需要添加-lws2_32,否则会报 “Undfined Reference to WSAStartup @8”等错误,解释在这里。
-
-
编译运行hello_client_win.cpp
-
gcc hello_client_win.cpp -o hClientWIn -lws2_32
-
-
运行结果:
-
hServerWin 9190
-
-
-
基于Windows的I/O函数
-
Windows严格区分文件I/O函数和套接字I/O函数。
-
Winsock数据传输函数
#include <winsock2.h> int send(SOCKET s, const char *buf, int len, int flags);// 成功时返回传输字节数,失败返回SOCKET_ERROR
- s : 数据传输对象连接的套接字句柄
- buf : 保存待传输数据的缓存地址值
- len : 要传输的字节数
- flags:传输数据时用到的多种选项信息
#include <winsock2.h> int recv(SOCKET s, const char * buf , int len, int flags); // 成功返回接收的字节数,失败返回SOCKET_ERROR
- s : 数据传输对象连接的套接字句柄
- buf : 保存待传输数据的缓存地址值
- len : 要传输的字节数
- flags:传输数据时用到的多种选项信息