Socket编程之TCP套件字
基于的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
)
SOCKET(2) Linux Programmer's Manual SOCKET(2)NAMEsocket - create an endpoint for communicationSYNOPSIS#include <sys/types.h> /* See NOTES */#include <sys/socket.h>// windows的socket在 winsock2.hint socket(int domain, int type, int protocol);/*@描述:申请一个指定类型和指定协议的套接字@domain:指定域/协议簇。socket接口不仅不局限于TCP/IP,它可以用于Bluetooth、本地通
信...每一种下面都有自己的许多协议,我们把IPV4下面的所有协议都归纳到了一个域:AF_INET IPV4AF_INET6 IPV6AF_UNIX AF_LOCAL 本地通信AF_BULETOOTH 蓝牙...@type:指定要创建的套件字的类型:SOCK_STREAM 流式套接字SOCK_DGRAM 数据报套接字SOCK_RAW 原始套接字...TCP采用流式套接字,UDP采用数据报套接字@protocol协议,指定具体的应用层协议,可以指定为0:表示采用不知名的私有的应用层@return:成功返回一个套接字描述符失败返回-1,同时errno被设置*/
2.2.1.2 绑定一个网络地址:(bind
)
- 并不是任意的地址都可以(需要合法且能够正常访问)
- 把一个套接字和一个网络地址进行绑定。如果想让其他人来主动联系/连接,就需要绑定一个地
址,并且需要把这个地址告诉其他人。不进行绑定,并代表套接字没有地址,不进行绑定套接字在
进行通信时候,内核会动态为套接字指定一个地址。
BIND(2) Linux Programmer's Manual
BIND(2)NAMEbind - bind a name to a socketSYNOPSIS#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; // 指定为IPV4sock_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
)
- 让一个套接字进入一个监听状态
LISTEN(2) Linux Programmer's Manual
LISTEN(2)NAMElisten - listen for connections on a socketSYNOPSIS#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连接
- 三次握手
- 函数成功返回表示和一个客户端完成连接
- 多次调用函数就可以与不同的客户端进行连接
ACCEPT(2) Linux Programmer's Manual
ACCEPT(2)NAMEaccept, accept4 - accept a connection on a socketSYNOPSIS#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
#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)
- 四次挥手
#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
CONNECT(2) Linux Programmer's Manual
CONNECT(2)NAMEconnect - initiate a connection on a socketSYNOPSIS#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
示例
客户端
#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. 申请一个套机字 socketint 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; // 地址结构 指定为IPV4addr.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;
}
服务端
#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; // 指定为IPV4addrSrv.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
并不是服务器本身,而是服务器为每个客户端创建的一个独立的套接字,用于和客户端进行数据传输。