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() 函数。
(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() 函数进行初始化,并指明要使用的版本号
最新的DLL是 ws2_32.dll,大小为 69KB,对应的头文件为 winsock2.h。
使用#pragma命令,在编译时加载:
#pragma comment (lib, "ws2_32.lib")
WinSock 编程的第一步就是加载 ws2_32.dll,然后调用 WSAStartup() 函数进行初始化,并指明要使用的版本号
WSADATA wsaData; WSAStartup( MAKEWORD(2, 2), &wsaData);
4. socket编程
(1) 使用socket()函数创建套接字
(2) 使用bind()函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理
(3) 使用connect()函数来建立连接
(2) 使用bind()函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理
(3) 使用connect()函数来建立连接
(4) sockaddr结构体
可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体
另外还有 sockaddr_in6,用来保存 IPv6 地址
可以认为,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()
(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回声服务器的实现
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <sys/socket.h> 7 8 #define BUF_SIZE 1024 9 void error_handling(char *message); 10 11 int main(int argc, char *argv[]) 12 { 13 int serv_sock, clnt_sock; 14 char message[BUF_SIZE]; 15 int str_len, i; 16 17 struct sockaddr_in serv_adr; 18 struct sockaddr_in clnt_adr; 19 socklen_t clnt_adr_sz; 20 21 if(argc!=2) { 22 printf("Usage : %s <port> ", argv[0]); 23 exit(1); 24 } 25 26 serv_sock=socket(PF_INET, SOCK_STREAM, 0); 27 if(serv_sock==-1) 28 error_handling("socket() error"); 29 30 memset(&serv_adr, 0, sizeof(serv_adr)); 31 serv_adr.sin_family=AF_INET; 32 serv_adr.sin_addr.s_addr=htonl(INADDR_ANY); 33 serv_adr.sin_port=htons(atoi(argv[1])); 34 35 if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1) 36 error_handling("bind() error"); 37 38 if(listen(serv_sock, 5)==-1) 39 error_handling("listen() error"); 40 41 clnt_adr_sz=sizeof(clnt_adr); 42 43 for(i=0; i<5; i++) 44 { 45 clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); 46 if(clnt_sock==-1) 47 error_handling("accept() error"); 48 else 49 printf("Connected client %d ", i+1); 50 51 while((str_len=read(clnt_sock, message, BUF_SIZE))!=0) 52 write(clnt_sock, message, str_len); 53 54 close(clnt_sock); 55 } 56 57 close(serv_sock); 58 return 0; 59 } 60 61 void error_handling(char *message) 62 { 63 fputs(message, stderr); 64 fputc(' ', stderr); 65 exit(1); 66 }
(2)基于linux环境的tcp回声客户端的实现
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <sys/socket.h> 7 8 #define BUF_SIZE 1024 9 void error_handling(char *message); 10 11 int main(int argc, char *argv[]) 12 { 13 int sock; 14 char message[BUF_SIZE]; 15 int str_len; 16 struct sockaddr_in serv_adr; 17 18 if(argc!=3) { 19 printf("Usage : %s <IP> <port> ", argv[0]); 20 exit(1); 21 } 22 23 sock=socket(PF_INET, SOCK_STREAM, 0); 24 if(sock==-1) 25 error_handling("socket() error"); 26 27 memset(&serv_adr, 0, sizeof(serv_adr)); 28 serv_adr.sin_family=AF_INET; 29 serv_adr.sin_addr.s_addr=inet_addr(argv[1]); 30 serv_adr.sin_port=htons(atoi(argv[2])); 31 32 if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1) 33 error_handling("connect() error!"); 34 else 35 puts("Connected..........."); 36 37 while(1) 38 { 39 fputs("Input message(Q to quit): ", stdout); 40 fgets(message, BUF_SIZE, stdin); 41 42 if(!strcmp(message,"q ") || !strcmp(message,"Q ")) 43 break; 44 45 write(sock, message, strlen(message)); 46 str_len=read(sock, message, BUF_SIZE-1); 47 message[str_len]=0; 48 printf("Message from server: %s", message); 49 } 50 51 close(sock); 52 return 0; 53 } 54 55 void error_handling(char *message) 56 { 57 fputs(message, stderr); 58 fputc(' ', stderr); 59 exit(1); 60 }
5.Ack号 = Seq号 + 传递的字节数 + 1
6.
(1) 调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据
(2) 使用 shutdown() 函数可以只断开一条数据传输通道,而保留另一条
(3) 默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包, 也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。
(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 为参数。
- SD_RECEIVE:关闭接收操作,也就是断开输入流。
- SD_SEND:关闭发送操作,也就是断开输出流。
- SD_BOTH:同时关闭接收和发送操作。
(3) 默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包, 也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。
例子:
(1)file_server.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <sys/socket.h> 7 8 #define BUF_SIZE 30 9 void error_handling(char *message); 10 11 int main(int argc, char *argv[]) 12 { 13 int serv_sd, clnt_sd; 14 FILE * fp; 15 char buf[BUF_SIZE]; 16 int read_cnt; 17 18 struct sockaddr_in serv_adr, clnt_adr; 19 socklen_t clnt_adr_sz; 20 21 if(argc!=2) { 22 printf("Usage: %s <port> ", argv[0]); 23 exit(1); 24 } 25 26 fp=fopen("file_server.c", "rb"); 27 serv_sd=socket(PF_INET, SOCK_STREAM, 0); 28 29 memset(&serv_adr, 0, sizeof(serv_adr)); 30 serv_adr.sin_family=AF_INET; 31 serv_adr.sin_addr.s_addr=htonl(INADDR_ANY); 32 serv_adr.sin_port=htons(atoi(argv[1])); 33 34 bind(serv_sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); 35 listen(serv_sd, 5); 36 37 clnt_adr_sz=sizeof(clnt_adr); 38 clnt_sd=accept(serv_sd, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); 39 40 while(1) 41 { 42 read_cnt=fread((void*)buf, 1, BUF_SIZE, fp); 43 if(read_cnt<BUF_SIZE) 44 { 45 write(clnt_sd, buf, read_cnt); 46 break; 47 } 48 write(clnt_sd, buf, BUF_SIZE); 49 } 50 51 shutdown(clnt_sd, SHUT_WR); 52 read(clnt_sd, buf, BUF_SIZE); 53 printf("Message from client: %s ", buf); 54 55 fclose(fp); 56 close(clnt_sd); close(serv_sd); 57 return 0; 58 } 59 60 void error_handling(char *message) 61 { 62 fputs(message, stderr); 63 fputc(' ', stderr); 64 exit(1); 65 }
(2)file_client.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <sys/socket.h> 7 8 #define BUF_SIZE 30 9 void error_handling(char *message); 10 11 int main(int argc, char *argv[]) 12 { 13 int sd; 14 FILE *fp; 15 16 char buf[BUF_SIZE]; 17 int read_cnt; 18 struct sockaddr_in serv_adr; 19 if(argc!=3) { 20 printf("Usage: %s <IP> <port> ", argv[0]); 21 exit(1); 22 } 23 24 fp=fopen("receive.dat", "wb"); 25 sd=socket(PF_INET, SOCK_STREAM, 0); 26 27 memset(&serv_adr, 0, sizeof(serv_adr)); 28 serv_adr.sin_family=AF_INET; 29 serv_adr.sin_addr.s_addr=inet_addr(argv[1]); 30 serv_adr.sin_port=htons(atoi(argv[2])); 31 32 connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); 33 34 while((read_cnt=read(sd, buf, BUF_SIZE ))!=0) 35 fwrite((void*)buf, 1, read_cnt, fp); 36 37 puts("Received file data"); 38 write(sd, "Thank you", 10); 39 fclose(fp); 40 close(sd); 41 return 0; 42 } 43 44 void error_handling(char *message) 45 { 46 fputs(message, stderr); 47 fputc(' ', stderr); 48 exit(1); 49 }
7. recv() 返回 0 的唯一时机就是收到FIN包时
8. 网络字节序
不同 CPU 保存和解析数据的方式不同(主流的 Intel 系列 CPU 为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。
不同 CPU 保存和解析数据的方式不同(主流的 Intel 系列 CPU 为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。
9.
ping 域名可以查看域名对应的IP地址
nslookup命令可以查看计算机中注册的默认DNS服务器地址
ping 域名可以查看域名对应的IP地址
nslookup命令可以查看计算机中注册的默认DNS服务器地址
10.
(1) 下列函数可以通过传递字符串格式的域名获取IP地址
View Code
len: 向第一个参数传递的地址信息的字节数,IPv4时为4,IPv6时为6
family: 传递地址族信息,IPv4时为AF_INET, IPv6时为AF_INET6
(1) 下列函数可以通过传递字符串格式的域名获取IP地址
#include <netdb.h> struct hostent *gethostbyname(const char *hostname);
成功时返回hostnet结构体指针,失败时返回NULL指针
例子:gethostbyname.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <arpa/inet.h> 5 #include <netdb.h> 6 void error_handling(char *message); 7 8 int main(int argc, char *argv[]) 9 { 10 int i; 11 struct hostent *host; 12 if(argc!=2) { 13 printf("Usage : %s <addr> ", argv[0]); 14 exit(1); 15 } 16 17 host=gethostbyname(argv[1]); 18 if(!host) 19 error_handling("gethost... error"); 20 21 printf("Official name: %s ", host->h_name); 22 23 for(i=0; host->h_aliases[i]; i++) 24 printf("Aliases %d: %s ", i+1, host->h_aliases[i]); 25 26 printf("Address type: %s ", 27 (host->h_addrtype==AF_INET)?"AF_INET":"AF_INET6"); 28 29 for(i=0; host->h_addr_list[i]; i++) 30 printf("IP addr %d: %s ", i+1, 31 inet_ntoa(*(struct in_addr*)host->h_addr_list[i])); 32 return 0; 33 } 34 35 void error_handling(char *message) 36 { 37 fputs(message, stderr); 38 fputc(' ', stderr); 39 exit(1); 40 }
struct hostnet { char *h_name; char **h_aliases; int h_addrtype; int h_lenght; char **h_addr_list; }
(2) gethostbyaddr()函数利用IP地址获取域相关信息
#include <netdb.h> 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
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <netdb.h> 7 void error_handling(char *message); 8 9 int main(int argc, char *argv[]) 10 { 11 int i; 12 struct hostent *host; 13 struct sockaddr_in addr; 14 if(argc!=2) { 15 printf("Usage : %s <IP> ", argv[0]); 16 exit(1); 17 } 18 19 memset(&addr, 0, sizeof(addr)); 20 addr.sin_addr.s_addr=inet_addr(argv[1]); 21 host=gethostbyaddr((char*)&addr.sin_addr, 4, AF_INET); 22 if(!host) 23 error_handling("gethost... error"); 24 25 printf("Official name: %s ", host->h_name); 26 27 for(i=0; host->h_aliases[i]; i++) 28 printf("Aliases %d: %s ", i+1, host->h_aliases[i]); 29 30 printf("Address type: %s ", 31 (host->h_addrtype==AF_INET)?"AF_INET":"AF_INET6"); 32 33 for(i=0; host->h_addr_list[i]; i++) 34 printf("IP addr %d: %s ", i+1, 35 inet_ntoa(*(struct in_addr*)host->h_addr_list[i])); 36 return 0; 37 } 38 39 void error_handling(char *message) 40 { 41 fputs(message, stderr); 42 fputc(' ', stderr); 43 exit(1); 44 }
(3)字节序转换函数
unsigned short htons(unsigned short); unsigned short ntohs(unsigned short); unsigned long htonl(unsigned long); unsigned long ntohl(unsigned long);
(4) 将字符串信息转换为网络字节序的整数型
#include <arpa/inet.h> in_addr_t inet_addr(const char *string);
成功时返回32位大端序整数型值,失败时返回INADDR_NONE
(5) inet_aton()也将字符串形式IP地址转换位32位网络字节序整数并返回,只不过该函数利用in_addr结构体,且其使用频率跟高#include <arpa/inet.h> int inet_aton(const char *string, struct in_addr *addr);
成功时返回1(true),失败时返回0(false)
(6) inet_ntoa()函数可以把网络字节序整数型IP地址转换成字符串形式#include <arpa/inet.h> 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 "9190"; memset(&addr, 0, 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状态下的套接字端口号重新分配给新的套接字
12.多进程
(1) 通过调用fork函数创建进程
#include <unistd.h> pid_t fork(void);
父进程:fork函数返回子进程的ID
子进程:fork函数返回0(2) 应当向创建子进程的父进程传递子进程的exit参数值或return语句的返回值,防止僵尸进程的产生
销毁僵尸进程1:利用wait函数
销毁僵尸进程1:利用wait函数
#include <unistd.h> pid_t wait(int *statloc);
->成功时返回终止的子进程ID,失败时返回-1
子进程终止时传递的返回值将保存到statloc所指内存空间,需要用下列宏进行分离- WIFEXITED 子进程正常终止时返回“真”true
- WEXITSTATUS 返回子进程的返回值
#include <sys/wait.h> 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 <signal.h> void (*signal(int signal, void (*func)(void)))(int);
->为了在产生信号时调用,返回之前注册的函数指针
(6) 利用sigaction函数进行信号处理#include <signal.h> int sugacyion(int signo, const struct sigaction *act, struct sigaction *oldact);
->成功时返回0,失败时返回-1
通过fork函数复制套接字文件描述符后,同一端口将对应多个套接字,只有这些套接字描述符都终止,才能销毁套接字13.进程间通信
创建管道的函数:
#include <unistd.h> int pipe(int filedes[2]);
->成功时返回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 <sys/select.h> #include <sys/time.h> 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函数用于控制文件描述符
(5) 调用recv函数的同时传递MSG_PEEK可选项,是为了保证即使不存在待读取的数据也不会进入阻塞状态,设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除,该选项通常与MSG_DONTWAIT合作,用于调用以非阻塞方式验证待读取数据存在与否的函数
fcntl(recv_sock, F_SETOWN, getpid());
上述调用的含义是“将文件描述符recv_sock指向的套接字拥有者(F_SETOWN)改为把getpid函数返回值用作ID的进程
(4) 紧急指针指向紧急消息的下一个位置(偏移量+1),紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息(5) 调用recv函数的同时传递MSG_PEEK可选项,是为了保证即使不存在待读取的数据也不会进入阻塞状态,设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除,该选项通常与MSG_DONTWAIT合作,用于调用以非阻塞方式验证待读取数据存在与否的函数