C++ socket通信
服务端
- 步骤如下:
- 根据版本启动socket
- 根据返回值验证是否成功启动
- 调用socket创建套接字(指定通讯方式)
- 调用bind函数将套接字绑定到指定ip地址和端口
- 调用listen设定客户端链接数
- 调用accept等待客户端的连接
- 调用recv和send函数接收和发送数据
- 调用closesocket关闭socket
- 调用WSACleanup回收资源。
代码如下:
| |
| |
| #include <iostream> |
| #include <Winsock2.h> |
| |
| using namespace std; |
| |
| #pragma comment(lib,"ws2_32.lib") |
| |
| int main() |
| { |
| WSADATA wsaData; |
| SOCKET sockSrv; |
| SOCKADDR_IN addrSrv; |
| SOCKADDR_IN addrClient; |
| int len = sizeof(SOCKADDR); |
| int err; |
| WORD wVersionRequested; |
| wVersionRequested = MAKEWORD(2, 2); |
| err = WSAStartup(wVersionRequested, &wsaData); |
| if (err != 0) |
| { |
| cout << "启动使用Winsock DLL失败" << endl; |
| return 0; |
| } |
| if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) |
| { |
| WSACleanup(); |
| cout << "启动使用Winsock DLL失败" << endl; |
| return 0; |
| } |
| sockSrv = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); |
| addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); |
| addrSrv.sin_family = AF_INET; |
| addrSrv.sin_port = htons(6000); |
| bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); |
| listen(sockSrv, 5); |
| SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len); |
| |
| |
| |
| |
| |
| |
| cout << "欢迎你加入" << endl; |
| send(sockConn, "欢迎你加入", 20, 0); |
| while (true) |
| { |
| char sendBuf[256]; |
| char recvBuf[256]; |
| recv(sockConn, recvBuf, strlen(recvBuf),0); |
| Sleep(1000); |
| if (strlen(recvBuf) > 0) |
| { |
| cout << recvBuf << endl; |
| cout << "Input:"; |
| cin >> sendBuf; |
| send(sockConn, sendBuf, strlen(sendBuf) + 1, 0); |
| } |
| } |
| closesocket(sockConn); |
| WSACleanup(); |
| system("pause"); |
| } |
| |
客户端
| #define _WINSOCK_DEPRECATED_NO_WARNINGS |
| #include<iostream> |
| #include <Winsock2.h> |
| #include<stdlib.h> |
| using namespace std; |
| #pragma comment(lib, "ws2_32.lib") |
| int main() |
| { |
| WORD wVersinRequested; |
| WSADATA wsaData; |
| int err; |
| wVersinRequested = MAKEWORD(2, 2); |
| err = WSAStartup(wVersinRequested, &wsaData); |
| if (err != 0) |
| { |
| cout << "启动使用Winsock DLL失败" << endl; |
| return 0; |
| } |
| if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) |
| { |
| WSACleanup(); |
| cout << "启动使用Winsock DLL失败" << endl; |
| return 0; |
| } |
| SOCKADDR_IN addrSrv; |
| addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); |
| addrSrv.sin_family = AF_INET; |
| addrSrv.sin_port = htons(6000); |
| SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0); |
| connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); |
| Sleep(2000); |
| while (1) |
| { |
| char recvBuf[100]; |
| char sendBuf[100]; |
| recv(sockClient, recvBuf, 100, 0); |
| Sleep(1000); |
| if (strlen(recvBuf) > 0) |
| { |
| cout << recvBuf << endl; |
| cout << "Input:"; |
| cin >> sendBuf; |
| send(sockClient, sendBuf, strlen(sendBuf) + 1, 0); |
| } |
| } |
| closesocket(sockClient); |
| WSACleanup(); |
| system("pause"); |
| } |
| |
重要函数说明
| |
| |
| |
| int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData ); |
| |
| |
| |
| int WSACleanup (void); |
| |
| |
| |
| |
| int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
| |
| |
| |
| |
| |
| |
| |
| int listen(int sockfd, int backlog) |
| |
| |
| |
| |
| int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen); |
| |
| |
| |
| int send( SOCKET s, const char FAR *buf, int len, int flags ); |
| |
| |
| |
| |
| |
| |
| |
| int WSAAPI recv( |
| SOCKET s, |
| char *buf, |
| int len, |
| int flags |
| ); |
| |
| |
| |
| |
| int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
| |
| |
| |
| |
| |
| |
| closesocket( |
| _In_ SOCKET s |
| ); |
TCP socket的buffer
每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区,TCP的全双工的工作模式以及TCP的流量(拥塞)控制便是依赖于这两个独立的buffer以及buffer的填充状态。接收缓冲区把数据缓存入内核,应用进程一直没有调用recv()进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。再啰嗦一点,不管进程是否调用recv()读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。recv()所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面,并返回,仅此而已。进程调用send()发送的数据的时候,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send()返回之时,数据不一定会发送到对端去(和write写文件有点类似),send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中,发送是TCP的事情,和send其实没有太大关系。接收缓冲区被TCP用来缓存网络上来的数据,一直保存到应用进程读走为止。对于TCP,如果应用进程一直没有读取,接收缓冲区满了之后,发生的动作是:收端通知发端,接收窗口关闭(win=0)。这个便是滑动窗口的实现。保证TCP套接口接收缓冲区不会溢出,从而保证了TCP是可靠传输。因为对方不允许发出超过所通告窗口大小的数据。 这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方TCP将丢弃它。
send函数工作原理
send函数只负责将数据提交给协议层。 当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲区的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR; 如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据; 如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len; 如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完,如果len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。 如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR; 如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下