第二章、套接字类型与协议设置
2、1 套接字协议及其数据传输特性
-
协议(Protocol): 为完成数据交换而定好的约定。
-
创建套接字:
-
#include <sys/socket.h> int socket(int domain, int type, int protocol);// 成功时返回文件描述符,失败返回-1.
- domain : 套接字使用的协议族(Protocol family)信息。
- type : 套接字数据传输类型信息。
- protocol : 计算机通信使用的协议信息
-
-
协议族(protocol family):
- 套接字通信中的协议有一些分类,这些分类信息称为协议族。socket函数的第一个参数传递套接字中使用的协议分类信息。
- 书中重点介绍PF_INET(IPv4互联网协议簇),套接字实际采用的最终协议是由socket函数第三个参数传递的。
-
套接字类型(type):
- 指套接字的数据传输方式,通过socket函数第二个参数传递。只有指明了类型才能决定套接字数据传输方式。
-
套接字类型1:面向连接的套接字(SOCK_STREAM)
- 向socket函数第二个参数传递SOCK_STREAM将创建面向连接的套接字。
- 面向连接的套接字数据传输特征:
- 传输过程中数据不会消失。
- 按序传输数据。
- 传输的数据不存在数据边界。
- 套接字连接必须一一对应。
- 收发数据的套接字内部由缓冲(buffer)即字节数组,通过套接字传输的数据将保存到该数组。面向连接的套接字中,read函数和write函数的调用次数并无太大意义,所以说不存在数据边界。
- 当缓冲被填满后,套接字无法接收数据,此时传输端将停止传输。
- 面向连接的套接字会根据接收端的状态传输数据,传输出错还会重传。所以不会发生数据丢失。
- 面向连接的套接字只能与另一个同样特征的套接字连接。
- 面向连接的套接字数据传输特征:
- 可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字。
- 向socket函数第二个参数传递SOCK_STREAM将创建面向连接的套接字。
-
套接字类型2:面向消息的套接字(SOCK_DGRAM)
- 向socket函数第二个参数传递SOCK_DGRAM将创建面向消息的套接字。
- 面向消息的套接字数据传输特征:
- 强调快速传输而非传输顺序
- 传输的数据可能丢失也可能损毁
- 传输的数据有数据边界
- 限制每次传输的数据大小
- 面向消息的套接字比面向连接的套接字具有更快的传输速度,但是无法避免数据丢失或损毁,每次传输的数据大小具有一定的限制,并存在数据边界(意味着接收数据次数应和传输次数相同)
- 不可靠的、不按序传递的、以数据的高速传输为目的的套接字。
- 不存在连接的概念。
-
协议的最终选择:
- socket函数的第三个参数决定最终采用的协议。前两个参数传递了 协议族信息 和 套接字数据传输方式 即可创建所需套接字,大部分情况下可以向第三个参数传递0, 除非 同一个协议族中存在多个数据传输方式相同的协议。
- 数据传输方式相同,但协议不同,此时需要第三个参数具体指定协议信息。
-
例子:
-
“IPv4协议族中面向连接的套接字”
-
int tcp_socket = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);// TCP套接字
-
-
“IPv4协议族中面向消息的套接字”
-
int udp_socket = socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP); // UDP套接字
-
-
-
面向连接的套接字:TCP套接字示例:
-
验证TCP套接字的特性:传输的数据不存在数据边界。
-
为验证此,需要使write函数的调用次数不同于read函数的调用次数。在客户端中多次调用read函数以接收服务器端发送的全部数据。
-
服务器端 tcp_server.c
-
#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); }
-
客户端 tcp_client.c
-
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> void error_handing(char *message); int main(int argc,char*argv[]){ int sock; struct sockaddr_in serv_addr; char message[30]; int str_len = 0; int idx = 0, read_len = 0; if(argc!= 3){ printf("Usage : %s <IP> <port>\n",argv[0]); exit(1); } // 创建 IPv4协议族中面向连接的套接字, 创建TCP套接字,前两个参数是PF_INET、SOCK_STREAM时,可以省略IPPROTO_TCP sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP); if(sock == -1) error_handing("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_handing("connect() error"); // read函数读取数据,成功时返回接收的字节数,失败返回-1 // 反复调用read函数,每次读取1字节,read返回0,说明遇到了文件结尾, while(read_len=read(sock,&message[idx++],1)){ if(read_len == -1) error_handing("read() error"); // 累加read调用接收到的信息的字节数。 str_len += read_len; } printf("Message from server : %s \n", message); // 由于一次读取一个字节,所以传递了多少数据说明调用了多少此read printf("Function read call count : %d \n",str_len); close(sock); return 0; } void error_handing(char *message){ fputs(message,stderr); fputc('\n',stderr); exit(1); }
-
可以看到服务器端发送了13字节的数据,客户端调用了13次read函数进行读取。
-
2、2 Windows平台下的实现与验证
-
windows系统的socket函数
-
#include <winsock2.h> SOCKET socket(int af, int type, int protocol); // 成功返回socket句柄,失败返回INVALID_SOCKET
-
socket函数返回类型是SOCKET,结构体用来保存整数型套接字句柄值。失败时返回的INVALID_SOCKET可以理解为一个常数。
-
-
基于Windows的TCP套接字示例:
-
服务器端
-
#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 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); }
-
-
客户端
-
#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 = 0; int idx = 0, readlen = 0; 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函数接收服务器端发送来的数据 while (readlen = recv(hSocket, &message[idx++], 1, 0)) { if (readlen == -1) ErrorHanding("read() error"); strlen += readlen; } printf("Message from serve : %s \n", message); printf("Function read call count : %d \n", strlen); closesocket(hSocket); WSACleanup(); return 0; } void ErrorHanding(const char* message) { fputs("message", stderr); fputc('\n', stderr); exit(1); }
-
-
结果:
-