1 理解TCP和UDP
- 其中四层模型说的就是TCP(UDP)/IP协议栈
![三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议](https://imgs.yssmx.com/Uploads/2024/01/808393-1.png)
![三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议](https://imgs.yssmx.com/Uploads/2024/01/808393-2.png)
![三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议](https://imgs.yssmx.com/Uploads/2024/01/808393-3.png)
2 补充函数解释
![三:基于TCP的服务端/客户端,# TCP/IP网络编程,tcp/ip,网络协议](https://imgs.yssmx.com/Uploads/2024/01/808393-4.png)
- listen详解
#include <sys/socket.h>
/**
* @param[1]: 希望进入等待状态的套接字
* @param[2]: 连接请求等待队列的长度
*/
int listen(int sock, int backlog);
当客户端发送连接请求时,并不一定能立即连接到。尽管此时服务端处于等待连接请求状态(listen),但是由于系统正忙,此连接请求需要进入连接请求等待队列,listen第二个参数便是设置此等待队列的大小。
- accept函数详解
#include <sys/socket.h>
/**
* @brief : 受理连接请求等待队列中待处理的连接请求。
* @param[1] : sock:服务器套接字的文件描述符;
* @param[2] : addr:用于保存发起连接请求的客户端地址信息;
* @param[3] : addrlen:第二个参数的长度。
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);
- accept 函数会受理连接请求等待队列中待处理的客户端连接请求,它从等待队列中取出 1 个连接请求,创建套接字并完成连接请求。如果等待队列为空,accpet 函数会阻塞,直到队列中出现新的连接请求才会返回。
- 它会在内部产生一个新的套接字并返回其文件描述符,该套接字用于与客户端建立连接并进行数据 I/O。新的套接字是在 accept 函数内部自动创建的,并自动与发起连接请求的客户端建立连接。
accept 执行完毕后会将它所受理的连接请求对应的客户端地址信息存储到第二个参数 addr 中。
- connect() 函数详解
#include <sys/socket.h>
/**
* @brief : 请求连接。
* @param[2] : 保存目标服务器端地址信息的结构体指针
*/
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
客户端调用 connect 函数后会阻塞,直到发生以下情况之一才会返回:
- 服务器端接收连接请求。
- 发生断网等异常情况而中断连接请求。
注意:上面说的”接收连接请求“并不是服务器端调用了 accept 函数,而是服务器端把连接请求信息记录到等待队列。因此 connect 函数返回后并不立即进行数据交换。
3 实现迭代回声服务端和客户端(问题版)
迭代回声服务器端与回声客户端的基本运行方式:
- 服务器端同一时刻只与一个客户端相连接,并提供回声服务。
- 服务器端依次向 5 个客户端提供服务,然后退出。
- 客户端接收用户输入的字符串并发送到服务器端。
- 服务器端将接收到的字符串数据传回客户端,即”回声“。
- 服务器端与客户端之间的字符串回声一直执行到客户端输入 Q 为止。
服务端代码
注:由于不同公司代码规范的规定不同,所以书写方式可能跟书本有出入,不过不影响内容一致
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
const int kBufferSize = 1024;
void ErrorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]) {
int server_sock;
int client_sock;
char message[kBufferSize];
int str_len = 0;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_addr_size;
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
server_sock = socket(PF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
ErrorHandling("socket() error");
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
ErrorHandling("bind() error");
}
if (listen(server_sock, 5) == -1) {
ErrorHandling("listen() error");
}
client_addr_size = sizeof(client_addr);
//循环变量i C11可以写里面了
for (int i = 0; i < 5; i++) {
client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_size);
if (client_sock == -1) {
ErrorHandling("accept() error");
} else {
printf("connected client %d \n", i + 1);
}
while ((str_len = read(client_sock, message, kBufferSize)) != 0) {
write(client_sock, message, str_len);
}
close(client_sock);
}
close(server_sock);
return 0;
}
客户端代码
提前说明存在的问题:
在本章的回声客户端的实现中有上面这段代码,它有一个错误假设:每次调用 read、write 函数时都会执行实际的 I/O 操作。
但是注意:TCP 是面向连接的字节流传输,不存在数据边界。所以多次 write 的内容可能一直存放在发送缓存中,某个时刻再一次性全都传递到服务器端,这样的话客户端前几次 read 都不会读取到内容,最后才会一次性收到前面多次 write 的内容。还有一种情况是服务器端收到的数据太大,只能将其分成多个数据包发送给客户端,然后客户端可能在尚未收到全部数据包时旧调用 read 函数。
理解:问题的核心在于 write 函数实际上是把数据写到了发送缓存中,而 read 函数是从接收缓存读取数据。并不是直接对 TCP 连接的另一方进行数据读写。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
const int kBufferSize = 1024;
void ErrHandling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[]) {
int sock;
char message[kBufferSize];
int str_len;
struct sockaddr_in serv_addr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) {
ErrHandling("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]));
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
ErrHandling("connect() error");
} else {
puts("Connected...........");
}
while (1) {
fputs("Input message(Q to quit): ", stdout);
fgets(message, kBufferSize, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
break;
}
write(sock, message, strlen(message));
str_len = read(sock, message, kBufferSize - 1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
基于win的实现
将 Linux 平台下的实例转化为 Windows 下的实例,记住以下四点:
- 通过 WSAStartup、WSACleanup 函数初始化并清除套接字相关库。
- 把数据类型和变量名切换为 Windows 风格。
- 数据传输中用 recv、send 函数而非 read、write 函数。
- 关闭套接字时用 closesocket 函数而非 close 函数。
4 迭代回声服务端/客户端(优化)
由于write和read都是有缓冲的,所以写完直接只读一次,可能读不全。注意:一定能读到,因为read是阻塞IO函数。所以为了读全,需要循环去读。
客户端代码优化
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
const int kBufferSize = 1024;
void ErrHandling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[]) {
int sock;
char message[kBufferSize];
int recv_len = 0;
int str_len;
int recv_cnt = 0;
struct sockaddr_in serv_addr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) {
ErrHandling("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]));
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
ErrHandling("connect() error");
} else {
puts("Connected...........");
}
while (1) {
fputs("Input message(Q to quit): ", stdout);
fgets(message, kBufferSize, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
break;
}
str_len = write(sock, message, strlen(message)); //发送的字节数
recv_len = 0;
while (recv_len < str_len) {
recv_cnt = read(sock, message, kBufferSize - 1);
if (recv_cnt == -1) {
ErrHandling("read() error");
}
recv_len += recv_cnt;
}
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
新问题:如果不知道接收的数据的长度
上面的回声客户端中,提前就知道要接收的数据长度,但是一般是不知道的。这种情况下,要解决拆包和粘包的问题,就要定义应用层协议。
应用层协议实际就是在服务器端/客户端的实现过程中逐步定义的规则的集合。
在应用层协议中可以定好数据边界的表示方法、数据的长度范围等。
代码体验定义应用层协议
程序描述如下
服务端从客户端获取多个数字和运算符信息。服务端搜索到数字后进行运算,将结果传回客户端。例如给服务端发送3、5、9、* 则服务端返回359的结果
我们定义的协议如下:
- 客户端用 1 个字节整数形式传递操作数的个数。
- 客户端向服务器端传送的每个操作数占用 4 字节。
- 传递完操作数后紧跟着传递一个占用 1 字节的运算符。
- 服务器端以 4 字节整数向客户端传回运算结果。
- 客户端得到运算结果后终止与服务器端的连接。
先展示效果
自己瞎写写
- op_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
const int kBufferSize = 1024;
void ErrHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int Calculate(int op_num, int *op_param, char op_character)
{
int res = op_param[0];
switch (op_character) {
case '+':
for (int i = 1; i < op_num; i++) {
res += op_param[i];
}
break;
case '-':
for (int i = 1; i < op_num; i++) {
res -= op_param[i];
}
break;
case '*':
for (int i = 1; i < op_num; i++) {
res *= op_param[i];
}
break;
default:
return 0;
}
return res;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
int server_sock = -1;
int client_sock = -1;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
//1
server_sock = socket(PF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
ErrHandling("socket() error");
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
//2:
if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
ErrHandling("bind() error");
}
//3:
if (listen(server_sock, 5) == -1) {
ErrHandling("listen() error");
}
//4: 此处就处理一个连接,如果想处理多个连接,将1改大点
for (int i = 0; i < 1; i++) {
client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_sock == -1) {
ErrHandling("accept() error");
}
int op_num = 0;
read(client_sock, &op_num, 1); //读取一个字节
//接下来接收,4*op_num + 1个字节的数据
int recv_len = 0;
int recv_cnt = 0;
char op_data[kBufferSize];
while (recv_len < (4*op_num + 1)) {
//不断更新数据应该写入的位置
recv_cnt = read(client_sock, &op_data[recv_len], kBufferSize -1);
recv_len += recv_cnt;
}
int res = Calculate(op_num, (int*)op_data, op_data[recv_len - 1]);
write(client_sock, (void*)&res, sizeof(res));
close(client_sock);
}
close(server_sock);
return 0;
}
- op_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
const int kBufferSize = 1024;
void ErrHandling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[]) {
int sock;
char message[kBufferSize];
int recv_len = 0;
int str_len;
int recv_cnt = 0;
struct sockaddr_in serv_addr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
//1st
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) {
ErrHandling("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]));
//2nd
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
ErrHandling("connect() error");
} else {
puts("Connected...........");
}
char msg[kBufferSize]; //存放要发的数据
fputs("Operand count: ", stdout);
int op_cnt = 0;
scanf("%d", &op_cnt);
msg[0] = (char)op_cnt;
for (int i = 0; i < op_cnt; i++) {
printf("Operand %d: ", i + 1);
scanf("%d", (int*)&msg[i*4 + 1]);
}
//删除缓冲区中的\n,因为下边要输入字符。这里看结尾的链接就行,好久不写C,我一开始也没注意
fgetc(stdin);
fputs("Operator: ", stdout);
scanf("%c", &msg[op_cnt * 4 + 1]);
write(sock, msg, op_cnt*4 + 2);
int res;
read(sock, &res, 4);
printf("Message from server: %d", res);
close(sock);
return 0;
}
链接: 为什么scanf之前要使用fgetc接受一个字符
5 TCP原理
TCP套接字中的IO缓冲
如前所述,TCP套接字的数据收发无边界,所以数据不是发了就一定能立即全部收到。
Actually,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据,都要经过缓冲区。
套接字 I/O 缓冲的特性:
- I/O 缓冲在每个套接字中单独存在。
- I/O 缓冲在创建套接字时自动生成。
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
- 关闭套接字将丢失输入缓冲中的数据。
注:不会发生超过输入缓冲区大小的数据传输,即:服务器发多少数据是跟客户端询问过的。文章来源:https://www.toymoban.com/news/detail-808393.html
6 基于windows的实现
https://gitee.com/pipe-man/tcp_ip_socket/tree/master/%E6%BA%90%E4%BB%A3%E7%A0%81
自取吧,懒得看Windows了,工作用不到win,用到再补。文章来源地址https://www.toymoban.com/news/detail-808393.html
到了这里,关于三:基于TCP的服务端/客户端的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!