C++利用Socket实现主机间的UDP/TCP通信
cout丶shy 人气:0前言
完整代码放到github上了:cppSocketDemo
服务器端的代码做了跨平台(POSIX和WINDOWS),基于POSIX平台(Linux、Mac OS X、PlayStation等)使用sys/socket.h库,windows平台使用winsock2.h库。
客户端代码因为基本都在windows运行,所以没做跨平台,需要的话你可以参考服务器端代码自己做一下。
文中写的函数原型均为windows平台,部分函数的返回类型或参数类型在POSIX会有不同。
头文件
根据_WIN32标志区分,导入头文件。
#include<iostream> #include<cstring> #ifdef _WIN32 #include<winsock2.h> #else #include <sys/socket.h> #include <netinet/in.h> #include <sys/types.h> #include <netdb.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> typedef int SOCKET; #endif
因为POSIX平台的socket库没有SOCKET类型,所以我们手动定义一下。
UDP Socket
服务器端
对于windows,使用socket前需要手动开启:
#ifdef _WIN32 WSADATA wsd; if(WSAStartup(MAKEWORD(2, 2), &wsd)){ std::cout << "WSAStartup Error" << std::endl; exit(-1); } #endif
WSAStartup第一个参数表示使用版本号。该函数会向第二个参数填入被激活的socket库的信息。
SOCKET函数
SOCKET socket(int af, int type, int protocol);
参数:
af:socket使用协议族,如AF_INET表示IPv4, AF_INET6表示IPv6等。
type:指明通过socket发送和接收分组的形式。如SOCK_STREAM表示有序、可靠的数据流分段;SOCK_DGRAM表示离散的报文;SOCK_RAW表示数据头部可以由应用层自定义。
protocol:指明发送数据使用什么协议。IPPROTO_UDP;IPPROTO_TCP;IPPROTO_IP;0表示根据socket类型选择默认协议。
通过socket函数创建并返回一个udp类型socket对象:
SOCKET udpSocket = socket(AF_INET, SOCK_DGRAM, 0);
bind函数
将一个socket绑定到一个地址和端口号,使用bind函数:
int bind(SOCKET sock, const sockaddr *address, int address_len);
参数:
sock: 绑定socket
address:注意是指发送数据包的源地址,而不是发送目的地址
address_len:存储address的sockaddr结构体大小
bind成功时返回0,出现错误时返回-1
给端口号赋值0,将告诉socket库找一个未被使用的端口并绑定
如果一个进程试图使用一个未绑定的socket发送数据,网络库将自动为这个socket绑定一个可用的端口号。
所以对于服务器来说手动调用bind绑定是必须的,而对于客户端来说通常是没有必要的。
服务器端socket需要显式绑定地址和端口,以便客户端访问:
sockaddr_in sain; sain.sin_family = AF_INET; //使用IPv4 sain.sin_addr.s_addr = htonl(INADDR_ANY); sain.sin_port = htons(atoi("50002")); if(bind(udpSocket, (sockaddr *)&sain, sizeof(sockaddr)) == -1){ std::cout << "绑定失败" << std::endl; }
recvfrom函数
从UDP Socket接收数据
int recvfrom(SOCKET s,char *buf,int len,int flags,struct sockaddr *from,int *fromlen);
参数:
s: 查询数据的socket。默认情况下,
buf: 接收的数据包的缓冲区。
len: buf可以存储的最大字节数。到达的数据包的剩余字节将被丢弃。
flags: 同sendto flags。
from: 指向发送者的地址和端口号的指针,该值由recvfrom函数写入(每接收一个数据包写入一次)。不要手动填写。
fromlen: from所指向sockaddr的大小
如果recvfrom成功执行会返回复制到buf的字节数,发生错误返回-1。
服务器通过recvfrom函数接收客户端信息:
const size_t BufMaxSize = 1000; char buf[BufMaxSize] = {}; sockaddr fromAddr; #ifndef _WIN32 unsigned #endif int fromAddrLen = sizeof(sockaddr); std::cout << "等待接收..." << std::endl; while(true){ if(recvfrom(udpSocket, buf, BufMaxSize, 0, &fromAddr, &fromAddrLen) != -1){ std::cout << "接收到数据:" << buf << std::endl; memset(buf, 0, sizeof(buf)); } else{ std::cout << "接收失败或发生错误!" << std::endl; return -1; } }
最后记得做关闭工作
#ifdef _WIN32 WSACleanup(); closesocket(udpSocket); #else close(udpSocket); #endif
客户端
和服务器一样的先激活:
WSADATA wsd; if(WSAStartup(MAKEWORD(2, 2), &wsd)){ std::cout << "WSAStartup Error" << std::endl; exit(-1); }
创建socket:
SOCKET udpSocket = socket(AF_INET, SOCK_DGRAM, 0);
将目标远程主机的IP和端口信息填入sockaddr:
先写一个工具函数:
sockaddr GetSockAddr(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4, uint16_t inPort){ sockaddr addr; sockaddr_in *addrin = reinterpret_cast<sockaddr_in*>(&addr); addrin->sin_family = AF_INET; addrin->sin_addr.S_un.S_un_b.s_b1 = b1; addrin->sin_addr.S_un.S_un_b.s_b2 = b2; addrin->sin_addr.S_un.S_un_b.s_b3 = b3; addrin->sin_addr.S_un.S_un_b.s_b4 = b4; addrin->sin_port = htons(inPort); return addr; }
sockaddr toAddr = GetSockAddr(127, 0, 0, 1, 50002);
sendTo函数
从UDP Socket发送数据
int sendto(SOCKET s,const char *buf,int len,int flags,const struct sockaddr *to,int tolen);
参数:
s: 数据包应该使用的socket,如果没有绑定,socket库将自动绑定一个可用的端口。
buf: 待发送数据的起始地址的指针。可以是任何能够被转为char*的数据类型。
len: 待发送数据的大小。尽量避免发送数据大于1300字节的数据包,详见p75。
flags: 对控制发送的标志进行按位或运算的结果,该值通常取0即可。
to: 目标接收者的sockaddr。注意to的地址族必须和用于创建socket的地址族一致。
tolen:to的sockaddr的大小。对于IPv4,传入sizeof(sockaddr_in)即可。
sendto操作成功返回等待发送的数据长度(说明成功进入发送队列),否则返回-1。
通过senTo函数发送数据:
const size_t BufMaxSize = 1000; char buf[BufMaxSize] = {}; sockaddr toAddr = GetSockAddr(127, 0, 0, 1, 50002); int toAddrLen = sizeof(sockaddr); std::cout << ">>> "; while(true){ if(std::cin >> buf){ sendto(udpSocket, buf, strlen(buf), 0, &toAddr, toAddrLen); std::cout << "已发送!" <<std::endl; std::cout << ">>> "; memset(buf, 0, sizeof(buf)); } }
注意,这样发送给linux服务器带中文的字符串的话,可能出现乱码,因为linux通常为UTF-8编码,而windows通常为gb2312编码,所以我们可以在客户端实现两个编码转换函数,并在恰当时机转换:
//UTF-8转GB2312 char* U2G(const char* utf8){ int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); wchar_t* wstr = new wchar_t[len+1]; memset(wstr, 0, len+1); MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr, len); len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len+1]; memset(str, 0, len+1); WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL); if(wstr) delete[] wstr; return str; } //GB2312转UTF-8 char* G2U(const char* gb2312){ int len = MultiByteToWideChar(CP_ACP, 0, gb2312, -1, NULL, 0); wchar_t* wstr = new wchar_t[len+1]; memset(wstr, 0, len+1); MultiByteToWideChar(CP_ACP, 0, gb2312, -1, wstr, len); len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len+1]; memset(str, 0, len+1); WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, NULL, NULL); if(wstr) delete[] wstr; return str; }
发送改为:
char *buf_UTF8 = G2U(buf); sendto(udpSocket, buf_UTF8, strlen(buf_UTF8), 0, &toAddr, toAddrLen);
最后同样的关闭操作:
closesocket(udpSocket); WSACleanup();
测试
注意,如果把服务器代码放到windows下执行,记得把客户端的转编码代码改下。
注意,编译使用C++11以上编译,链接时加入库:
-lwsock32
将udpServer.cpp放到服务器上,服务器防火墙记得放行目标端口或暂时关闭防火墙。
udpClient.cpp在本地(windows)。
udpClient中的目标远程主机地址改为服务器ip地址,编译运行:
服务器:
客户端:
TCP Socket(单客户端连接)
服务器
同样先激活winsock:
#ifdef _WIN32 WSADATA wsd; if(WSAStartup(MAKEWORD(2, 2), &wsd)){ std::cout << "WSAStartup Error" << std::endl; return -1; } #endif
创建tcp类型socket:
SOCKET tcpsocket = socket(AF_INET, SOCK_STREAM, 0);
绑定本机地址和指定端口号:
sockaddr_in sain; sain.sin_family = AF_INET; sain.sin_addr.s_addr = htonl(INADDR_ANY); sain.sin_port = htons(atoi("50002")); if(bind(tcpsocket, (sockaddr*)&sain, sizeof(sockaddr)) == -1){ std::cout << "bind Error" << std::endl; return -1; }
listen函数
用listen函数启动监听,等待客户端的连接:
int listen(SOCKET sock, int backlog);
backlog指队列允许传入的最大连接数,超过最大值的连接都将被丢弃。可以使用SOMAXCONN表示默认的backlog值。
函数执行成功返回0,失败返回-1。
使用listen函数开启监听:
listen(tcpsocket, 10);
主机针对每个保持的TCP连接,都需要一个独立的socket存储连接状态。
这里先做只能连接单个客户端,创建连接客户端的socket:
sockaddr clientAddr; // #ifndef _WIN32 unsigned #endif int clientAddrLen = sizeof(sockaddr);
accept函数
接受传入的连接并继续TCP握手过程,调用accept函数:
SOCKET accept(SOCKET sock, sockaddr* addr, int* addrlen);
参数:
sock: 接收传入连接的监听socket
addr: 将被写入请求连接的远程主机地址。同样不要手动填写
addrlen: 指向addr缓冲区大小的指针。当真正写入地址之后,accept会更新该值。
如果accept执行成功,将创建并返回一个可以与远程主机通信的新socket。
接受传入的连接并继续TCP握手过程:
SOCKET linkSocket = accept(tcpsocket, &clientAddr, &clientAddrLen);
recv函数
调用recv函数从一个连接的TCP socket接收数据:
int recv(SOCKET s,char *buf,int len,int flags);
参数:
s: 待接收数据的socket
buf: 数据接收缓冲区。
len: 拷贝到buf中的数据的最大数量。
flags: 标志位,大多数情况填0。
调用成功返回接收的数据大小。如果返回0,说明连接的另一端发送了一个FIN数据包,承诺没有更多需要发送的数据。
如果发生错误,返回-1
默认情况下,如果socket的接收缓冲区中没有数据,recv函数将阻塞调用线程,直到数据流中的下一组数据到达或超时。
send函数
通过连接的socket使用send函数发送数据:
因为连接的socket存储了远程主机地址信息,所以不需要传入地址参数:
int send(SOCKET s,const char *buf,int len,int flags);
参数:
s: 用于发送数据的socket
buf: 写入缓冲区。注意:和UDP不同,是将数据放到socket的输出缓冲区中,由socket库来决定在将来某一时刻发出。
len: 传输的字节数量。注意:与UDP不同,不需要保持这个值低于链路层的MTU。
flags:标志位,大多数情况下填0即可。
send调用成功返回发送数据的大小,如果发送错误返回-1.
默认情况下该函数会阻塞线程,直到调用超时或发送了足够的数据。
非0的返回值不代表成功发送出去了,只说明数据被存入队列中等待发送。
使用recv函数和send函数接收和响应客户端信息:
const size_t BufMaxLen = 1000; char buf[BufMaxLen] = {}; char sendBuf[BufMaxLen] = "服务器成功接收!"; while(true){ int ret = recv(linkSocket, buf, BufMaxLen, 0); if(ret > 0){ std::cout << "从客户端接收到数据:" << buf << std::endl; memset(buf, 0, BufMaxLen); send(linkSocket, sendBuf, strlen(sendBuf), 0); } else if(ret == 0){ std::cout << "客户端停止发送数据,准备关闭连接..." << std::endl; break; } else{ std::cout << "recv发生错误!" << std::endl; } }
最后关闭:
#ifdef _WIN32 closesocket(tcpsocket); closesocket(linkSocket); WSACleanup(); #else close(tcpsocket); close(linkSocket); #endif std::cout << "已关闭服务器Socket..." << std::endl;
客户端
客户端没有新函数,直接看代码吧!
TCPSocketClient.cpp:
#include<iostream> #include<winsock2.h> #include<windows.h> #include<memory> #include<cstring> using namespace std; //UTF-8转GB2312 char* U2G(const char* utf8){ int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); wchar_t* wstr = new wchar_t[len+1]; memset(wstr, 0, len+1); MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr, len); len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len+1]; memset(str, 0, len+1); WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL); if(wstr) delete[] wstr; return str; } //GB2312转UTF-8 char* G2U(const char* gb2312){ int len = MultiByteToWideChar(CP_ACP, 0, gb2312, -1, NULL, 0); wchar_t* wstr = new wchar_t[len+1]; memset(wstr, 0, len+1); MultiByteToWideChar(CP_ACP, 0, gb2312, -1, wstr, len); len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); char* str = new char[len+1]; memset(str, 0, len+1); WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, NULL, NULL); if(wstr) delete[] wstr; return str; } sockaddr GetSockAddr(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4, uint16_t inPort){ sockaddr addr; sockaddr_in *addrin = reinterpret_cast<sockaddr_in*>(&addr); addrin->sin_family = AF_INET; addrin->sin_addr.S_un.S_un_b.s_b1 = b1; addrin->sin_addr.S_un.S_un_b.s_b2 = b2; addrin->sin_addr.S_un.S_un_b.s_b3 = b3; addrin->sin_addr.S_un.S_un_b.s_b4 = b4; addrin->sin_port = htons(inPort); return addr; } int main(){ WSADATA wsd; if(WSAStartup(MAKEWORD(2, 2), &wsd)){ std::cout << "WSAStartup Error" << std::endl; return -1; } SOCKET tcpSocket = socket(AF_INET, SOCK_STREAM, 0); sockaddr serverAddr = GetSockAddr(127, 0, 0, 1, 50002); int serverAddrLen = sizeof(sockaddr); if(connect(tcpSocket, &serverAddr, serverAddrLen) == -1){ std::cout << "connect Error!" << std::endl; return -1; } std::cout << "已成功连接到服务器" << std::endl; //客户端的socket就是用于连接的socket const int BufMaxLen = 1000; char sendBuf[BufMaxLen] = {}; char buf[BufMaxLen] = {}; std::cout << ">>> "; while(true){ if(std::cin >> sendBuf){ if(strcmp(sendBuf, "exit") == 0){ std::cout << "正在关闭连接..." << std::endl; break; } char *sendBuf_UTF8 = G2U(sendBuf); send(tcpSocket, sendBuf_UTF8, strlen(sendBuf_UTF8), 0); memset(sendBuf, 0, BufMaxLen); int ret = recv(tcpSocket, buf, BufMaxLen, 0); if(ret > 0){ std::cout << "从服务器接收到回应:" << U2G(buf) << std::endl; memset(buf, 0, BufMaxLen); } std::cout << ">>> "; } } shutdown(tcpSocket, SB_BOTH); closesocket(tcpSocket); WSACleanup(); std::cout << "已关闭客户端Socket..." << std::endl; return 0; }
测试
注意,如果把服务器代码放到windows下执行,记得把客户端的转编码代码改下。
测试方式同上面UDP。
客户端:
服务器:
TCP Socket(多客户端连接)
服务端
使用多线程,每响应一个客户端连接为它创建一个线程。
多线程头文件:
#include<thread>
将之前的响应代码搬到函数中作为线程入口:
void linkClientThread(SOCKET linkSocket, unsigned int linkId){ printf("客户端(id:%d) 已连接!\n", linkId); const size_t BufMaxLen = 1000; char buf[BufMaxLen] = {}; char sendBuf[BufMaxLen] = "服务器成功接收!"; while(true){ int ret = recv(linkSocket, buf, BufMaxLen, 0); if(ret > 0){ printf("从客户端(id:%d)接收到数据:%s\n", linkId, buf); memset(buf, 0, BufMaxLen); send(linkSocket, sendBuf, strlen(sendBuf), 0); } else if(ret == 0){ printf("客户端(id:%d)停止发送数据,关闭连接...\n", linkId); break; } else{ printf("recv发生错误!\n"); break; } } #ifdef _WIN32 closesocket(linkSocket); #else close(linkSocket); #endif }
当接收到连接请求,为它单独创建一个线程服务:
while(true){ SOCKET linkSocket = accept(tcpsocket, &clientAddr, &clientAddrLen); std::thread linkThread(linkClientThread, linkSocket, ++linkId); linkThread.detach(); }
客户端
客户端直接继续使用之前的tcpClient.cpp即可,没有区别。
测试
同样的注意,如果把服务器代码放到windows下执行,记得把客户端的转编码代码改下。
服务器还是使用linux系统的,所有客户端在本地的windows执行:
注意:server代码在linux编译时要加入-lpthread.h选项:
g++ -g tcpServer_multiConnection.cpp -o tcpServer_multiConnection -std=c++11 -lpthread
客户端1:
客户端2:
服务器:
加载全部内容