基于的TCP套件字编程流程
1. Socket套接字
Socket
是一个编程接口(网络编程接口 ),是一种特殊的文件描述符(write/read)。Socket并不 仅限于TCP/IP
Socket
独立于具体协议的编程接口,这个接口位于TCP/IP
四层模型的应用层与传输层之间
介绍一下网络通信的三种主要模式
- 单工 A B两个端 A只负责发送 B只负责接收
- 半双工 A、B都可以发送或者接受 但是同一时间点之只能发送或者接收
- 全双工 A、B同一时间点既可以发也可以收
不使用多线程和并发的话socket编程 只能支持到半双工
1.2 Socket
的类型
- 流式套接字:(
SOCK_STREAM
)- 面向字节流,针对于传输层协议为
TCP
协议的网络应用
- 面向字节流,针对于传输层协议为
- 数据报套接字:(
SOCK_DGRAM
)- 面向数据报,针对于传输层协议为
UDP
协议的网络应用
- 面向数据报,针对于传输层协议为
- 原始套接字:(
SOCK_RAW
)- s直接跳过传输层
2.基于的TCP套件字编程流程
任何网络应用都会有通信双方:
Send
发送端recv
接收端
TCP网络应用(C/S模型)(长连接)
Client
客户端(TCP
)Serve
r 服务端(TCP
)
任何的网络应用:
传输层的协议(TCP/UDP
)+ 端口 + IP
地址
网络地址:
任何网络应用任意一方都需要有一个网络地址 (IP
+端口0)
2.1 TCP网络应用执行的过程
- 建立连接
- 三次握手
- 发送/接收数据
- 发送数据:
write/send/sendto
- 接收数据:
read/recv/recvfrom
- 发送数据:
- 关闭连接
- 四次挥手
2.2 TCP网络应用的编程流程
2.2.1 TCP-Server服务端
2.2.1.1 建立一个套件字:(socket
)
c
SOCKET(2) Linux Programmer's Manual SOCKET(2)
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
// windows的socket在 winsock2.h
int socket(int domain, int type, int protocol);
/*
@描述:
申请一个指定类型和指定协议的套接字
@domain:
指定域/协议簇。socket接口不仅不局限于TCP/IP,它可以用于Bluetooth、本地通
信...
每一种下面都有自己的许多协议,我们把IPV4下面的所有协议都归纳到了一个域:
AF_INET IPV4
AF_INET6 IPV6
AF_UNIX AF_LOCAL 本地通信
AF_BULETOOTH 蓝牙
...
@type:
指定要创建的套件字的类型:
SOCK_STREAM 流式套接字
SOCK_DGRAM 数据报套接字
SOCK_RAW 原始套接字
...
TCP采用流式套接字,UDP采用数据报套接字
@protocol
协议,指定具体的应用层协议,可以指定为0:表示采用不知名的私有的应用层
@return:
成功返回一个套接字描述符
失败返回-1,同时errno被设置
*/
2.2.1.2 绑定一个网络地址:(bind
)
- 并不是任意的地址都可以(需要合法且能够正常访问)
- 把一个套接字和一个网络地址进行绑定。如果想让其他人来主动联系/连接,就需要绑定一个地
址,并且需要把这个地址告诉其他人。不进行绑定,并代表套接字没有地址,不进行绑定套接字在
进行通信时候,内核会动态为套接字指定一个地址。
c
BIND(2) Linux Programmer's Manual
BIND(2)
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/*
@描述:
用于给一个指定的套接字绑定网络地址
@sockfd:
需要绑定地址的套接字
@addr:
一个结构体类型,表示网络地址
socket接口不仅可以用于以太网(IPV4),也可以用于IPV6,同时也可以
用于Bluetooth,....
不同的协议簇,它的地址是不一样的。
socket编程接口,用一个通用的 " 网络地址接口 "
struct sockaddr
{
sa_family_t sin_family; // 指定协议簇
char sa_data[14];
};
协议地址结构:
struct sockaddr_in
{
sa_family_t sin_family; // 指定协议簇
u_int16_t sin_port; // 端口号
struct in_addr sin_addr;// IP地址
char sin_zero[8]; // 填充8字节,为了和其他协
议簇地址结构体大小一样
};
如:
struct sockaddr_in sock_info;
sock_info.sin_family = AF_INET; // 指定为IPV4
sock_info.sin_port = htons(6666); //指定为6666端
口
sock_info.sin_addr.s_addr =
inet_addr("192.168.31.1"); // 绑定ip地址
// inet_aton("192.168.31.1",&sock_info.sin_addr);
bind(sock,(struct sockaddr
*)&sock_info,sizeof(sock_info));
@addrlen
表示网络地址结构体的大小
@return:
成功返回0,失败返回-1
*/
**struct sockaddr_inj解释一下
**这是他的头文件include <arpa/inet.h>
-
sockaddr_in
绑定网络地址-
sock
-》socket
套接字 -
add
r -》address
地址 -
in
-》internet
网络
-
2.2.1.3 等待监听:(listen
)
- 让一个套接字进入一个监听状态
c++
LISTEN(2) Linux Programmer's Manual
LISTEN(2)
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
/*
@描述
设置指定的套接字进入监听模式
@sockfd:
需要进入监听模式的套接字
@backlog:
可以处理的最大请求数目,可以理解为发起请求的客户端的队伍可以有多长
@return:
成功返回0,失败返回-1
*/
2.2.1.4 等待客户端的连接:(accept
)
- 等待客户端来发起连接和客户端建立TCP连接
- 三次握手
- 函数成功返回表示和一个客户端完成连接
- 多次调用函数就可以与不同的客户端进行连接
c
ACCEPT(2) Linux Programmer's Manual
ACCEPT(2)
NAME
accept, accept4 - accept a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
@描述:
等待客户端连接套接字,等待客户端发起连接请求
@sockfd:
等待客户端连接的那个套接字
@addr:
网络地址结构体,用于存储连接成功的客户端信息的。
@addrlen:
网络地址结构体的长度指针,用来保存客户端地址结构体的长度的。
在调用的时候addrlen指向的空间保存的是addr的结构体的最大长度。
如果函数成功返回,addrlen指向的空间保存的是client客户端地址的结构体长
度。
@return:
成功返回与该客户端的连接套接字的描述符(后续服务端和客户端的数据通信,通
过该套件字通信 )
失败返回-1,同时errno被设置。
2.2.1.5 数据的传输:读/写
- 发送数据:
write/send/sendto
- 接收数据:
read/recv/recvfrom
c++
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/*
作用:
往指定套接字中写入数据
@sockfd:
需要写入数据的套接字描述符
@buf:
需要写入的数据空间的指针
@len:
数据的长度
@flags:
一般给0," 带外数据 "
@return:
成功返回实际发送的字节数,失败返回-1,同时...
*/
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/*
作用:
从指定套接字中获取数据
@sockfd:
需要读取数据的套接字描述符
@buf:
读取到的数据所要保存的空间的指针
@len:
需要获取的数据的长度
@flags:
一般给0," 带外数据 "
@return:
成功返回实际获取的字节数,失败返回-1,同时...
*/
2.2.1.6 关闭套接字:(close/shutdown)
- 四次挥手
c++
#include <sys/socket.h>
int shutdown(int sockfd, int how);
/*
作用:
关闭一个套接字
@sockfd:
需要关闭操作的套接字描述符
@how:
关闭方式:
SHUT_RD 关闭读
SHUT_WR 关闭写
SHUT_RDWR 关闭读写 -->close(sockfd);
*/
2.2.2 TCP-Client客户端
-
建立一个套接字:
socket
-
绑定地址:可选
- 可以绑定也可也不绑定(不推荐绑定,让系统分配)
-
发起连接请求:
connect
c
CONNECT(2) Linux Programmer's Manual
CONNECT(2)
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t
addrlen);
/*
@描述:
用指定的套接字,对指定网络地址发起连接请求
@sockfd:
发起连接请求的套接字
同时这个套接字是与服务端进行数据通信的套接字
@addr:
需要连接到的网络地址,目标地址
@addrlen:
目标地址结构体的大小
@return:
成功返回0,失败返回-1
*/
数据的传输:读/写
- 发送数据:
write/send/sendto
- 接收数据:
read/recv/recvfrom
关闭套接字 :close
示例
客户端
c++
#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cstring>
using namespace std;
vector<int> clients;
// 子线程 接收客户信息
void *myClient(void *arg)
{
int newClient = *(int *)arg;
// 等待客户端传过来数据
char buffer[1024] = {0};
while (1)
{
if (recv(newClient, buffer, 1024, 0) < 0)
{
bool shouldExit = false;
cout << "接收客户端信息失败" << endl;
close(newClient);
break;
}
if (strcmp(buffer, "exit") == 0)
{
cout << "结束通信" << endl;
close(newClient);
break;
}
// 转发给其他客户端
for (int i = 0; i < clients.size(); i++)
{
if (clients[i] == newClient)
continue;
if (send(clients[i], buffer, 1024, 0) < 0)
{
cout << "转发给其他客户端失败" << endl;
}
}
cout << "客户端信息:" << buffer << endl;
}
close(newClient);
return nullptr;
}
int main()
{
// 1. 申请一个套机字 socket
int socket_id = socket(AF_INET, SOCK_STREAM, 0);
if (socket_id == -1)
{
perror("套接字创建失败");
}
cout << "创建套接字成功" << endl;
// 2.绑定一个网络地址:(bind)
// struct sockaddr_in
/*
sockaddr_in 绑定网络地址
sock -》`socket`套接字
addr -》`address` 地址
in -》`internet `网络
*/
struct sockaddr_in addr;
addr.sin_family = AF_INET; // 地址结构 指定为IPV4
addr.sin_port = htons(5555); // 端口号
addr.sin_addr.s_addr = inet_addr("192.168.5.128"); // IP地址
// C 语言允许将非 const 类型的指针隐式地转换为 const 类型的指针会隐式转换吗
if ((bind(socket_id, (const struct sockaddr *)&addr, sizeof(addr)) == -1))
{
perror("绑定失败");
return -1;
}
cout << "绑定成功" << endl;
// 3. 等待监听:(listen)
if (listen(socket_id, 5) == -1)
{
perror("监听失败");
return -1;
}
cout << "监听成功" << endl;
cout << "服务请求开启成功" << endl;
// 4. 等待客户端的连接:(accept)
while (1)// 使用标志变量控制循环
{
struct sockaddr_in newClentInfor;
socklen_t addrlen = sizeof(sockaddr_in);
int newClent = accept(socket_id, (struct sockaddr *)&newClentInfor, &addrlen);
if (newClent == -1)
continue;
cout << "新客户端连接成功[" << inet_ntoa(newClentInfor.sin_addr) << ":" << htons(newClentInfor.sin_port) << "]" << endl;
// 增加一个客户端
clients.push_back(newClent);
// 创建一个线程
pthread_t tid;
pthread_create(&tid, nullptr, myClient, (void *)&newClent);
pthread_detach(tid); // 线程分离,避免阻塞主线程
}
// 关闭套接字 socket本质是文件标识符 close可以关闭
close(socket_id);
return 0;
}
服务端
c++
#include <winsock2.h>
#include <windows.h>
#include <iostream>
#include <pthread.h>
using namespace std;
// 接收信息的线程
void *RecvThread(void *arg)
{
// 套接字类型转换
SOCKET socket_client = *(SOCKET *)arg;
while(1)
{
char szMsg[256] = {0};
int nlen = recv(socket_client, szMsg, sizeof(szMsg), 0);
// 清空输入缓冲区
fflush(stdout);
if(nlen > 0)
{
// 清空输入缓冲区
fflush(stdout);
cout << "接收到的消息:" << szMsg << endl;
}
else if(nlen == 0)
{
cout << "服务器关闭" << endl;
break;
}
}
return nullptr;
}
int main()
{
// 1. 指定网络库版本
WSADATA waData;
// 2. 初始化网络库 MAKEWORD(2, 2) 表示请求 Winsock 2.2 版本
if (WSAStartup(MAKEWORD(2, 2), &waData) != 0)
{
perror("初始化网络库失败");
return -1;
}
// 申请一个套接字
SOCKET socket_client = socket(AF_INET, SOCK_STREAM, 0);
if (socket_client == INVALID_SOCKET)
{
perror("套接字申请失败");
return -1;
}
// 配置服务器地址
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET; // 指定为IPV4
addrSrv.sin_port = htons(5555); // 端口号
addrSrv.sin_addr.s_addr = inet_addr("192.168.5.128");
// 连接服务器
if (connect(socket_client, (sockaddr*)&addrSrv, sizeof(addrSrv)) == SOCKET_ERROR)
{
cerr << "连接失败,错误码:" << WSAGetLastError() << endl;
closesocket(socket_client);
WSACleanup();
return -1;
}
// 接收信息线程
pthread_t tid;
pthread_create(&tid, nullptr, RecvThread, (void*)&socket_client);
// 数据通信
while (1)
{
cout << "请输入:" << endl;
char szMsg[256] = {0};
cin >> szMsg;
if (send(socket_client, szMsg, strlen(szMsg), 0) == -1)
{
perror("发送失败");
return -1;
}
if (strcmp(szMsg, "exit") == 0)
{
cout << "通信结束";
// 关闭套接字
closesocket(socket_client);
// 释放网络库资源
WSACleanup();
exit(0);
}
}
// 关闭套接字
closesocket(socket_client);
// 释放网络库资源
WSACleanup();
return 0;
}
socket_id
:可以理解为服务器的"门岗",它负责监听指定的端口,等待客户端的连接请求。当有客户端请求连接时,socket_id
会接收到这个请求,并通过accept()
函数创建一个新的套接字来处理这个客户端。newClent
:这是通过accept()
函数创建的新套接字,它专门用于与连接的客户端进行通信。可以理解为服务器用来和特定客户端"对话"的通道。
socket_id
就像是一个门岗,负责接待来访的客户(监听连接请求);而newClent
就像是服务器派去和客户具体沟通的服务人员,负责处理具体的信息交流。不过要注意,
newClent
并不是服务器本身,而是服务器为每个客户端创建的一个独立的套接字,用于和客户端进行数据传输。