参考:
套接字通信部分
《TCP/IP 网络编程》以及《TCP/IP网络编程》学习笔记
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。
目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-Endian
和 Little-Endian
,下面先从字节序说起。
Little-Endian -> 主机字节序 (小端)
低位字节
存储到内存的低地址位
, 数据的高位字节
存储到内存的高地址位
Big-Endian -> 网络字节序 (大端)
低位字节
存储到内存的高地址位
, 数据的高位字节
存储到内存的低地址位
套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口
。以 PC 机为例:
int a = 0x12345678; // 从左往右,是从高位到低位
char *p = (char *) &a;
printf("sizeof(int, char) = %d, %d\n", sizeof(int), sizeof(char));
for(int i = 0; i < sizeof(int); ++i) {
printf("%d %p : 0x%02x\n", i, p, *p);
p++;
}
// 运行结果
/*
sizeof(int, char) = 4, 1
0 000000000070fe10 : 0x78
1 000000000070fe11 : 0x56
2 000000000070fe12 : 0x34
3 000000000070fe13 : 0x12
*/
BSD Socket 提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int
// 主机字节序 -> 网络字节序
u_short htons (u_short hostshort );
u_long htonl ( u_long hostlong);
// 网络字节序 -> 主机字节序
u_short ntohs (u_short netshort );
u_long ntohl ( u_long netlong);
// 主机字节序的 IP 地址是字符串, 网络字节序 IP 地址是整形
// linux函数, window上没有这两个函数
inet_ntop();
inet_pton();
// Windows上的
WSAAddressToStringA(); // 网络字节序 --> 主机字节序(点分十进制IP)
WSAStringToAddress(); // 主机字节序 --> 网络字节序
// windows 和 linux 都使用, 只能处理ipv4的ip地址
// 点分十进制IP -> 大端整形
unsigned long inet_addr (const char FAR * cp); // windows
in_addr_t inet_addr (const char *cp); // linux
// 大端整形 -> 点分十进制IP
// window, linux相同
char* inet_ntoa(struct in_addr in);
虽然 IP 地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的 IP 地址进行大小端转换:
主机字节序的IP地址 ---> 网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst);
参数:
af
: 地址族(IP地址的家族包括ipv4和ipv6)协议,AF_INET
: ipv4格式的ip地址, AF_INET6
: ipv6格式的ip地址src
: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.100dst
: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中返回值:成功返回1,失败返回0或者-1;返回0是异常, 说明src指向的不是一个有效的ip地址。
#include
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
af
: 地址族协议,AF_INET
: ipv4格式的ip地址,AF_INET6
: ipv6格式的ip地址src
: 传入参数,指向存储了大端的整形IP地址
的内存dst
: 传出参数, 指向存储了小端的点分十进制的IP地址
的内存size
: 修饰 dst 参数的, 标记 dst 指向的内存中最多可以存储多少个字节返回值:
AF_LOCAL
:表示的是本地地址
,对应的是 Unix 套接字,这种情况一般用于本地 socket 通信,很多情况下也可以写成 AF_UNIX
、AF_FILE
AF_INET
:因特网使用的 IPv4 地址AF_INET6
:因特网使用的 IPv6 地址这里的 AF_
表示的含义是 Address Family
,但是很多情况下,我们也会看到以PF_
表示的宏
,比如 PF_INET
、PF_INET6
等,实际上 PF_
的意思是 Protocol Family
,也就是协议族
的意思。我们用 AF_xxx
这样的值来初始化 socket 地址
,用 PF_xxx
这样的值来初始化 socket
。我们在
头文件中可以清晰地看到,这两个值本身就是一一对应的。
typedef unsigned short int sa_family_t;
struct sockaddr { //早期的 sockaddr
sa_family_t sa_family; /* adress family: AF_XXX */
char sa_data[14]; /* 14 bytes of protocol */
};
// struct sockaddr 很多网络编程API诞生早于IPv4协议,那时候都使用的是sockaddr结构体
// 为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数
// 至于这个函数是sockaddr_in还是其他的,由地址族确定,然后函数内部再强制转化为所需的地址类型。
sin_len
成员表示地址结构的长度,它是一个无符号的八位整数。需要强调的是,这个成员并不是地址结构必须有的。假如没有这个成员,其所占的一个字节被并入到 sin_family
成员中;同时,在传递地址结构的指针时,结构长度需要通过另外的参数来传递。
sin_family
成员指代的是所用的协议族,在有 sin_len
成员的情况下,它是一个8
位的无符号整数;在没有 sin_len
成员的情况下,它是一个16
位的无符号整数。由于IP协议属于TCP/IP协议族,所以在这里该成员应该赋值为 AF_INET
。
typedef uint32_t in_addr_t;
struct in_addr { // IPv4地址
in_addr_t s_addr; /* 32-bit IPv4 address; 网络字节序 */
};
struct sockaddr_in { //IPv4的 sockaddr
// 这个成员并不是地址结构必须有的
uint8_t sin_len; /* length of structure(地址结构) (16字节) */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16-bit TCP or UDP port number; 网络字节序 */
struct in_addr sin_addr; /* 32-bit IPv4 address; 网络字节序 */
// sin_zero成员是不使用的, 通常会将它置为0
// 它的存在只是为了与通用套接字地址结构 struct sockaddr 在内存中对齐
char sin_zero[8];/* unused */
};
由于sock API的实现早于ANSI C标准化,那时还没有 void *
类型,因此像 bind、accept
函数的参数都用 struct sockaddr *
类型表示, 在传递参数之前要强制转换一下如:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr*)&servaddr, sizeof(servaddr));
struct sockaddr_un {
unsigned short sun_family; /* 固定为 AF_LOCAL */
char sun_path[108]; /* 路径名 */
};
整个结构体长度是 28 个字节,其中流控信息和域 ID 先不用管,这两个字段,一个在 glibc 的官网上根本没出现,另一个是当前未使用的字段。这里的地址族显然应该是 AF_INET6
,端口同 IPv4 地址一样,关键的地址从 32 位升级到 128 位,这个数字就大到恐怖了,完全解决了寻址数字不够的问题。
struct sockaddr_in6 {
sa_family_t sin6_family; /* 16-bit */
in_port_t sin6_port; /* 传输端口号 # 16-bit */
uint32_t sin6_flowinfo; /* IPv6流控信息 32-bit*/
struct in6_addr sin6_addr; /* IPv6地址 128-bit */
uint32_t sin6_scope_id; /* IPv6域ID 32-bit */
};
使用套接字通信函数需要包含头文件
,包含了这个头文件
就不用在包含了。
// 创建一个套接字
int socket(int domain, int type, int protocol);
参数:
AF_INET
: 使用IPv4格式的ip地址,AF_INET6
: 使用IPv6格式的ip地址SOCK_STREAM
: 使用流式的传输协议;SOCK_DGRAM
: 使用报式(报文)的传输协议SOCK_STREAM
: 流式传输默认使用的是 TCP ;SOCK_DGRAM
: 报式传输默认使用的 UDPprotocol
来指定具体协议。PF_INET
(IPv4 协议族)下的 SOCK_STREAM
传输方式只对应 IPPROTO_TCP
一种协议,SOCK_DGRAM
传输方式也只对应 IPPROTO_UDP
一种协议,所以参数 protocol
只要设为 0 即可。通信的文件描述符
;失败: -1函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信是基于这个文件描述符来完成的。
给创建好的套接字分配地址信息(IP地址和端口号)
// 将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
: 文件描述符, 通过 socket()
调用得到的返回值addr
: 传入参数, 要绑定的 IP 和端口信息需要初始化到这个结构体中,IP 和端口要转换为网络字节序addrlen
: 参数 addr 指向的内存大小, sizeof(struct sockaddr)
返回值:成功返回 0,失败返回 -1
把套接字转换成可接受状态,进入等待连接请求状态,此时的套接字才是服务器端套接字
,此时的由socket返回的文件描述符才是用于监听的文件描述符
。
// 给监听的套接字设置监听
int listen(int sockfd, int backlog);
参数:
sockfd
: 文件描述符, 可以通过调用socket()
得到,在监听之前必须要绑定 bind()
backlog
: 同时能处理的最大连接要求,最大值为128返回值:函数调用成功返回 0,调用失败返回 -1
等待连接请求状态
:当服务器在此状态下时,在调用 accept
函数受理连接请求前,请求会处于等待状态。注意:这里说的是让来自客户端的请求处于等待状态,以等待服务器端受理它们的请求。
连接请求等待队列
:还未受理的连接请求在此排队,backlog
的大小决定了队列的最大长度,一般频繁接受请求的 Web 服务器的 backlog
至少为 15。
accept
函数会受理连接请求等待队列中待处理的客户端连接请求,它从等待队列中取出 1 个连接请求,创建套接字并完成连接请求。如果等待队列为空,accpet 函数会阻塞
,直到队列中出现新的连接请求才会返回。
它会在内部产生一个新的套接字并返回其文件描述符,该套接字用于与客户端建立连接并进行数据 I/O
。新的套接字是在 accept 函数内部自动创建的,并自动与发起连接请求的客户端建立连接。
accept 执行完毕后会将它所受理的连接请求对应的客户端地址信息存储到第二个参数 addr 中。
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd
: 监听的文件描述符
addr
: 传出参数, 里边存储了建立连接的客户端的地址信息addrlen
: 传入传出参数,用于存储addr指向的内存大小返回值:函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1
这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。
// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
参数:
sockfd
: 用于通信的文件描述符
, accept()
函数的返回值buf
: 指向一块有效内存, 用于存储接收数据size
: 参数 buf 指向的内存的容量flags
: 特殊的属性, 一般不使用, 指定为 0返回值:
>0
:实际接收的字节数0
:对方断开了连接-1
:接收数据失败了如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0。
// 发送数据
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
fd
: 通信的文件描述符, accept() 函数的返回值buf
: 传入参数, 要发送的字符串len
: 要发送的字符串的长度flags
: 特殊的属性, 一般不使用, 指定为 0返回值:
>0
:实际发送的字节数,和参数len是相等的-1
:发送数据失败了write 函数和 Windows 的 send 函数并不会在完成向对方主机的数据传输时返回,而是在数据移到输出缓冲时。但是 TCP 会保证对输出缓冲数据的传输,所以说 write 函数在数据传输完成时返回。
向服务器端发送连接请求
// 成功连接服务器之后, 客户端会自动随机绑定一个端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
: 通信的文件描述符, 通过调用 socket()
函数就得到了addr
: 存储了要连接的服务器端的地址信息: IP 和 端口,这个IP和端口也需要转换为大端然后再赋值addrlen
: addr指针指向的内存的大小 sizeof(struct sockaddr)返回值:连接成功返回 0,连接失败返回 -1
理解:接收端本来是不知道发送端的地址的,但调用完 recvfrom 函数后,发送端的地址信息就会存储到参数 src_addr
指向的结构体中。
// 接收数据, 如果没有数据,该函数阻塞
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd
: 基于udp的通信的文件描述符buf
: 指针指向的地址用来存储接收的数据len
: buf 指针指向的内存的容量, 最多能存储多少字节flags
: 设置套接字属性,一般使用默认属性,指定为 0
即可src_addr
: 发送数据的一端的地址信息,IP和端口都存储在这里边, 是大端存储的addrlen
: 类似于accept()
函数的最后一个参数, 是一个传入传出参数src_addr
参数指向的内存的大小, 传出的也是这块内存的大小src_addr
参数指定为NULL, 这个参数也指定为NULL即可返回值:成功返回接收的字节数,失败返回 -1
UDP 套接字不会保持连接状态,因此每次传输数据时都要添加目标地址信息
(相当于寄信前在信封上写收信地址)。
// 发送数据函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
sockfd
: 基于udp的通信的文件描述符buf
: 这个指针指向的内存中存储了要发送的数据len
: 要发送的数据的实际长度flags
: 设置套接字属性,一般使用默认属性,指定为 0
即可dest_addr
: 接收数据的一端对应的地址信息, 大端的IP和端口addrlen
: 参数 dest_addr
指向的内存大小返回值:成功返回实际发送的字节数,调用失败返回-1
TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
创建套接字后,并不马上分为服务端和客户端。如果紧接着调用 bind
、listen
函数,将成为服务器端套接字;如果调用 connect
函数,将成为客户端套接字。
accept
建立新的连接文件描述符
将内存中的数据读出, 这块内存称之为读缓冲区
文件描述符
将数据写入到某块内存中, 这块内存称之为写缓冲区
监听的文件描述符:
监听的文件描述符
的读缓冲区
中读缓冲区
中有数据, 说明有新的客户端连接accept()
函数, 这个函数会检测监听文件描述符的读缓冲区
通信的文件描述符:
write() / send()
,数据进入到内核中read() / recv()
, 从内核读数据在使用 read / write
函数对套接字进行读写数据时,实际上读写的是套接字输入 / 输出缓冲中的内容
。
套接字 I/O 缓冲的特性:
输出缓冲
中遗留的数据。输入缓冲
中的数据。要在 Windows 上进行套接字编程,需要:
client
在 windows 上还需要通过:项目–>属性–>配置属性–>C++ 将 SDL 检查设为否,否则使用旧函数inet_addr()
会报错。将 Linux 平台下的示例代码转换成 Windows 平台:
WSAStartup
、WSACleanup
函数初始化并清除套接字相关库recv / send
函数而非 read / write
函数closesocket
函数而非 close
函数没有新连接请求就阻塞
服务器端:
本文代码给出的都是 windows 系统下的,在命令行中执行类似如下代码:
hello_server_win 5000 # 在端口 5000 处接收连接请求
#include
#include
#include
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock; // windows系统下的,SOCKET就是int
SOCKADDR_IN servAddr, clntAddr;
int szClntAddr;
char message[] = "Hello World!";
if (argc != 2) // 检查参数数量
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 初始化 Winsock 相关库
ErrorHandling("WSAStartup() error!");
hServSock = socket(PF_INET, SOCK_STREAM, 0); // 创建套接字
if (hServSock == INVALID_SOCKET)
ErrorHandling("socket() error");
// 网络地址信息初始化
memset(&servAddr, 0, sizeof(servAddr)); // 主要为了把zero数组清空
servAddr.sin_family = AF_INET; // 设置协议族
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置 IP 地址 0.0.0.0
servAddr.sin_port = htons(atoi(argv[1])); // 设置端口号
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) // 为套接字分配地址和端口
ErrorHandling("bind() error");
if (listen(hServSock, 5) == SOCKET_ERROR) // 使套接字转换为可接收连接的状态
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr); // 接受连接请求,函数返回客户端的套接字
if (hClntSock == INVALID_SOCKET)
ErrorHandling("accept() error");
send(hClntSock, message, sizeof(message), 0); // 向客户端发送信息
closesocket(hClntSock); // 关闭客户端套接字
closesocket(hServSock); // 关闭服务器端套接字
WSACleanup(); // 注销 Winsock 相关库
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
初始化服务器端套接字时应分配所属计算机的IP地址,因为初始化时使用的IP地址非常明确,那为何还要进行IP初始化呢?如前所述,同一计算机中可以分配多个IP地址,实际IP地址个数与计算机中安装的NIC的数量相等。即使是服务器端套接字,也需要决定应接收那个IP传来的(哪个NIC传来的)数据。因此服务器端套接字初始化过程要求IP地址信息。另外,如果只有一个NIC,直接使用 INADDR_ANY。
在单线程的情况下客户端通信的文件描述符有一个, 没有监听的文件描述符
客户端:
在命令行中执行类似如下代码:
hello_client.exe 127.0.0.1 5000 # 向 127.0.0.1 5000 请求连接
#pragma execution_character_set("utf-8")
#include
#include
#include
void ErrorHandling(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 \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]); // 这里对书中代码进行了一些修改(源代码编译会报错,根据报错提示修改为当前代码)
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("connect() error!");
strLen = recv(hSocket, message, sizeof(message) - 1, 0);
if (strLen == -1)
ErrorHandling("read() error!");
printf("Message from server: %s \n", message);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
回声服务器端:它会将客户端传输的字符串数据原封不动地传回客户端,像回声一样。
实现迭代服务器端:调用一次 accept 函数只会受理一个连接请求,如果想要继续受理请求,最简单的方法就是循环反复调用 accept 函数,在前一个连接 close 之后,重新 accept。
在不使用多进程/多线程情况下,同一时间只能服务于一个客户端。
迭代回声服务器端与回声客户端的基本运行方式:
服务器端:
#include
#include
#include
void ErrorHandling(const char* message);
constexpr int BUF_SIZE = 1024;
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
int szClntAddr;
char message[BUF_SIZE];
int str_len;
if (argc != 2) // 检查参数数量
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 初始化 Winsock 相关库
ErrorHandling("WSAStartup() error!");
hServSock = socket(PF_INET, SOCK_STREAM, 0); // 创建套接字
if (hServSock == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET; // 设置协议族
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置 IP 地址
servAddr.sin_port = htons(atoi(argv[1])); // 设置端口号
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) // 为套接字分配地址和端口
ErrorHandling("bind() error");
if (listen(hServSock, 5) == SOCKET_ERROR) // 使套接字转换为可接收连接的状态
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
for (int i = 0; i < 5; ++i) {
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr); // 接受连接请求,函数返回客户端的套接字
if (hClntSock == INVALID_SOCKET)
ErrorHandling("accept() error");
else printf("Connnected client %d\n", i + 1);
while ((str_len = recv(hClntSock , message, BUF_SIZE, 0)) != 0) {
send(hClntSock, message, str_len, 0);
}
closesocket(hClntSock);
}
closesocket(hServSock); // 关闭服务器端套接字
WSACleanup(); // 注销 Winsock 相关库
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端:
#pragma execution_character_set("utf-8")
#include
#include
#include
void ErrorHandling(const char* message);
constexpr int BUF_SIZE = 1024;
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[BUF_SIZE];
int str_len;
if (argc != 3)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]); // 这里对书中代码进行了一些修改(源代码编译会报错,根据报错提示修改为当前代码)
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("connect() error!");
else printf("Connected....");
while (1) {
fputs("Input Message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
send(hSocket, message, strlen(message), 0);
str_len = recv(hSocket, message, BUF_SIZE - 1, 0);
if (str_len == -1)
ErrorHandling("read() error!");
message[str_len] = 0;
printf("Message from server: %s", message);
}
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
send(hSocket, message, strlen(message), 0);
str_len = recv(hSocket, message, BUF_SIZE - 1, 0);
在本章的回声客户端的实现中有上面这段代码,它有一个错误假设:每次调用 read、write 函数时都会执行实际的 I/O 操作。
但是注意:TCP 是面向连接的字节流传输,不存在数据边界。所以多次 write 的内容可能一直存放在发送缓存中,某个时刻再一次性全都传递到服务器端,这样的话客户端前几次 read 都不会读取到内容,最后才会一次性收到前面多次 write 的内容
。还有一种情况是服务器端收到的数据太大,只能将其分成多个数据包发送给客户端,然后客户端可能在尚未收到全部数据包时旧调用 read 函数
。
理解:问题的核心在于 write 函数实际上是把数据写到了发送缓存中,而 read 函数是从接收缓存读取数据。并不是直接对 TCP 连接的另一方进行数据读写。实际上就是没有考虑拆包和粘包的情况。
解决方法的核心: 提前确定接收数据的大小。
客户端上一次使用 write 从套接字发送了多少字节,紧接着就使用 read 从套接字读取多少字节。
// 接受完所以数据才打印
int recv_len = 0, recv_cnt;
while(recv_len < str_len) {
str_len = recv(hSocket, &message[recv_len], BUF_SIZE - 1, 0);
recv_len += recv_cnt;
}
message[str_len] = 0;
printf("Message from server: %s", message);
回声客户端可以提前知道接收的数据长度,但是更多情况下这不可能。这种情况下,要解决拆包和粘包的问题,就要定义应用层协议。
应用层协议实际就是在服务器端/客户端的实现过程中逐步定义的规则的集合。
在应用层协议中可以定好数据边界的表示方法、数据的长度范围等。
为实现计算器功能,需要定义一个简单的应用层协议,用来约定在服务器端和客户端之间传输数据的规则。
协议内容包括:
*
、 +
、 -
其中之一服务器端:
#include
#include
#include
void ErrorHandling(const char* message);
int Calculate(int cnt, int nums[], char op);
constexpr int BUF_SIZE = 1024;
constexpr int opsz = 4;
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
int szClntAddr;
char message[BUF_SIZE];
int str_len;
if (argc != 2) // 检查参数数量
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 初始化 Winsock 相关库
ErrorHandling("WSAStartup() error!");
hServSock = socket(PF_INET, SOCK_STREAM, 0); // 创建套接字
if (hServSock == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET; // 设置协议族
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置 IP 地址
servAddr.sin_port = htons(atoi(argv[1])); // 设置端口号
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) // 为套接字分配地址和端口
ErrorHandling("bind() error");
if (listen(hServSock, 5) == SOCKET_ERROR) // 使套接字转换为可接收连接的状态
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
int opnd_cnt = 0, recv_len = 0, recv_cnt, result;
for (int i = 0; i < 5; ++i) {
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr); // 接受连接请求,函数返回客户端的套接字
if (hClntSock == INVALID_SOCKET)
ErrorHandling("accept() error");
else printf("Connnected client %d\n", i + 1);
recv(hClntSock, (char*) &opnd_cnt, 1, 0);// 先读一个字节,读出数组元素个数
while (opnd_cnt * opsz + 1 > recv_len) {
// 用messgae存剩下的消息,最后一个字节为操作符
recv_cnt = recv(hClntSock, &message[recv_len], BUF_SIZE - 1, 0);
recv_len += recv_cnt;
}
result = Calculate(opnd_cnt, (int*)message, message[recv_len - 1]);
send(hClntSock, (char*)&result, sizeof(result), 0);
printf("end\n");
closesocket(hClntSock);
}
closesocket(hServSock); // 关闭服务器端套接字
WSACleanup(); // 注销 Winsock 相关库
return 0;
}
int Calculate(int cnt, int nums[], char op) {
int result = nums[0];
if (op == '+') {
for (int i = 1; i < cnt; ++i) result += nums[i];
}
else if (op == '-') {
for (int i = 1; i < cnt; ++i) result -= nums[i];
}
else {
for (int i = 1; i < cnt; ++i) result *= nums[i];
}
return result;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端:
#pragma execution_character_set("utf-8")
#include
#include
#include
void ErrorHandling(const char* message);
constexpr int BUF_SIZE = 1024;
constexpr int opsz = 4;
constexpr int rlt_size = 4;
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[BUF_SIZE];
int str_len;
if (argc != 3)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]); // 这里对书中代码进行了一些修改(源代码编译会报错,根据报错提示修改为当前代码)
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("connect() error!");
else printf("Connected....");
// 发送消息
int opnd_cnt, result;
fputs("Operand count:", stdout);
scanf("%d", &opnd_cnt);
message[0] = (char)opnd_cnt;
for (int i = 0; i < opnd_cnt; ++i) {
printf("Operand %d : ", i + 1);
scanf("%d", (int*)&message[i * opsz + 1]);
}
fgetc(stdin); // 吃掉回车
fputs("Operaotr: ", stdout);
scanf("%c", &message[opnd_cnt * opsz + 1]);
send(hSocket, message, opnd_cnt * opsz + 2, 0);
recv(hSocket, (char*) & result, rlt_size, 0);
printf("Operation result: %d\n", result);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
Linux 的 close 函数和 Windows 的 closesocket 函数都意味着完全断开连接。也就是无法发送也无法接收数据,有时候这不太优雅。
建立 TCP 套接字连接后可交换数据的状态可以看成一种流(包括输入流和输出流)。close 将会同时断开两个流。
断开一部分连接是指,可以传输数据但无法接受,或者可以接受数据但无法传输,也就是只关闭流的一半。
为何需要半关闭
一方在发送完所有数据后可以只关闭输出流但保留输入流,这样还可以接收对方的数据。
针对优雅断开的 shutdown 函数:
可以半关闭套接字
#include
int shutdown(int sock, int howto);
参数:
SHUT_RD / SD_RECEIVE
:断开输入流,此后套接字无法接收数据;SHUT_WR / SD_SEND
:断开输出流,此后套接字无法发送数据SHUT_RDWR / SD_BOTH
:同时断开 I/O 流。返回值:成功时返回 0,失败时返回 -1。
服务器端:
#include
#include
#include
#include
#define BUF_SIZE 30
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
FILE* fp;
char buf[BUF_SIZE];
int readCnt;
SOCKADDR_IN servAdr, clntAdr;
int clntAdrSz;
if (argc != 2) {
printf("Usage: %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
fp = fopen("D:\\practice\\VS\\half-close__file_server\\file_server_win.c", "rb");
hServSock = socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr));
listen(hServSock, 5);
clntAdrSz = sizeof(clntAdr);
hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSz);
while (1)
{
readCnt = fread((void*)buf, 1, BUF_SIZE, fp);
if (readCnt < BUF_SIZE)
{
send(hClntSock, (char*)&buf, readCnt, 0);
break;
}
send(hClntSock, (char*)&buf, BUF_SIZE, 0);
}
shutdown(hClntSock, SD_SEND);
recv(hClntSock, (char*)buf, BUF_SIZE, 0);
printf("Message from client: %s \n", buf);
fclose(fp);
closesocket(hClntSock); closesocket(hServSock);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端:
#include
#include
#include
#include
#define BUF_SIZE 30
void ErrorHandling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
FILE* fp;
char buf[BUF_SIZE];
int readCnt;
SOCKADDR_IN servAdr;
if (argc != 3) {
printf("Usage: %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
fp = fopen("D:\\practice\\VS\\half-close__file_server\\receive.dat", "wb");
hSocket = socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = inet_addr(argv[1]);
servAdr.sin_port = htons(atoi(argv[2]));
connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr));
while ((readCnt = recv(hSocket, buf, BUF_SIZE, 0)) != 0)
fwrite((void*)buf, 1, readCnt, fp);
puts("Received file data");
send(hSocket, "Thank you", 10, 0);
fclose(fp);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
UDP套接字的特点
区分 TCP 与 UDP 的一个典型比喻:UDP 好比寄信,TCP 好比打电话:
TCP 和 UDP 最重要的区别在于流控制
。
理解:这里的流控制应该包含了 TCP 的可靠传输、流量控制、拥塞控制等机制,这些机制都是在流上实现的。
UDP的高效使用
网络实时传输多媒体数据一般使用 UDP。
TCP 比 UDP 慢的两个原因:
进行连接的建立与释放
。流控制
。当收发的数据量小但需要频繁连接时,UDP 的高效体现地更明显
。
UDP服务器端和客户端均只需 1 个套接字
TCP 中,服务器端和客户端的套接字是一对一的关系,服务器端每向一个客户端提供服务,就需要分配一个新的套接字(accept创建的)。而 UDP 的服务器端和客户端均只需 1 个套接字,服务器端只要有一个 UDP 套接字就可以和多台主机通信。
UDP客户端套接字的地址分配
在 TCP 的客户端中 conncect
函数会自动完成给套接字分配 IP 地址和端口号的过程,UDP 中则是 sendto
函数来完成此功能。如果调用 sendto 函数时发现尚未给套接字分配地址信息,就会在首次调用 sendto 函数时给套接字分配 IP 地址和端口
。
存在数据边界的UDP套接字
UDP 套接字编程时,接收端输入函数的调用次数必须和发送端输出函数的调用次数相同,这样才能接收完发送端发送的数据。
已连接UDP套接字和未连接UDP套接字
通过 sendto 函数传输数据的过程包括三个阶段:
当多次通过 sendto 向同一个目标发送信息时,每次 sendto 都进行上面的步骤 1 和 3,就会很浪费时间。
因此当要长时间与同一主机通信时,将 UDP 变为已连接套接字会提高效率。
创建已连接 UDP 套接字
创建 UDP 套接字只需要对 UDP 套接字调用 connect
函数,但是这并不意味着要与对方的 UDP 套接字连接,这只是向 UDP 套接字注册目标 IP 和端口信息
。
connect(sock, (struct sockaddr*)&adr, sizeof(adr)); // 注意:adr 是目标的地址信息
使用已连接的 UDP 套接字进行通信时, sendto 函数就不会再执行步骤 1 和步骤 3,每次只要传输数据即可。因为已经指定了收发对象,所以不止可以用 sendto、recvfrom,也可以用 write、read 函数进行通信
。
使用 UDP 进行通信,服务器和客户端的处理步骤比 TCP 要简单很多,不存在请求连接和受理过程,并且两端是对等的 (通信的处理流程几乎是一样的),也就是说并没有严格意义上的客户端和服务器端,只是在提供服务的一端称为服务器端
。
在UDP通信过程中,服务器和客户端都可以作为数据的发送端和数据接收端,假设服务器端是被动接收数据,客户端是主动发送数据,那么在服务器端就必须绑定固定的端口了
。
服务器端可以同时与多个客户端进行通信
假设服务器端是接收数据的角色:
#include
#include
#include
#define BUF_SIZE 30
void error_handling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
int serv_sock;
char message[BUF_SIZE];
int str_len;
int clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr;
if (argc != 2)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 初始化 Winsock 相关库
error_handling("WSAStartup() error!");
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == -1)
error_handling("UDP socket creation error");
memset(&serv_adr, 0, 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[1]));
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
while (1)
{
clnt_adr_sz = sizeof(clnt_adr);
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); // 接收数据同时获取发送端地址
sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);
}
closesocket(serv_sock); // 上面的 while 是无限循环,这里的 colse 函数没什么实际意义。
WSACleanup();
return 0;
}
void error_handling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
假设客户端是发送数据的角色:
#include
#include
#include
#define BUF_SIZE 30
void error_handling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
int sock;
char message[BUF_SIZE];
int str_len;
int adr_sz;
struct sockaddr_in serv_adr, from_adr;
if (argc != 3)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 初始化 Winsock 相关库
error_handling("WSAStartup() error!");
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
while (1)
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
adr_sz = sizeof(from_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);
message[str_len] = 0;
printf("Message from server: %s", message);
}
closesocket(sock);
WSACleanup();
return 0;
}
void error_handling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
这是一个使用已连接 UDP 套接字的例子,在上边代码的基础上修改得到
#include
#include
#include
#define BUF_SIZE 30
void error_handling(const char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
int sock;
char message[BUF_SIZE];
int str_len;
int adr_sz;
struct sockaddr_in serv_adr, from_adr;
if (argc != 3)
{
printf("Usage : %s \n" , argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 初始化 Winsock 相关库
error_handling("WSAStartup() error!");
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); // 将套接字变为已连接套接字
while (1)
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
//sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
send(sock, message, strlen(message), 0);
adr_sz = sizeof(from_adr);
//str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);
str_len = recv(sock, message, BUF_SIZE, 0);
message[str_len] = 0;
printf("Message from server: %s", message);
}
closesocket(sock);
WSACleanup();
return 0;
}
void error_handling(const char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}