1 综述
因工作中用到Socket通信,基于网上已有大量相关Socket文章介绍,但提供的代码都较为简单,因此本文主要简单介绍一下Socket原理和Socket类中相关函数,并对网上现有的C++实现TCP/IP通信代码进行了部分改善,附在本文末尾,希望可以帮助入门者,如需更深入的功能,自行扩展。
参考博客:
Socket通信原理探讨(C++为例)
C++ socket通讯详解及注意事项
1.1 socket原理
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。因此socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
1.2 Socket库基本函数
(1)socket() 函数
int socket(int domain, int type, int protocol);
socket() 函数用于创建一个socket的套接字,它唯一标识一个socket。后续的操作都有用到这个套接字,把它作为参数,通过它来进行一些读写操作。
domain参数:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL…
type参数:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET等等
protocol参数:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等。当protocol为0时,会自动选择type类型对应的默认协议。
使用:int server_socket = socket(PF_INET, SOCK_STREAM, 0);—返回socket套接字
(2) bind() 函数
int bind( int sockfd, const struct sockaddr addr, socklen_t addrlen);
bind() 函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
sockfd参数:即socket套接字,它是通过socket()函数创建了,唯一标识一个socket.
addr参数:一个const struct sockaddr 指针,指向要绑定给sockfd的协议地址。
addrlen参数:对应的是地址的长度。
使用:bind(server_socket, (struct sockaddr)&server_addr, sizeof(server_addr))—返回0即表示成功
(3)listen()、connect()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
服务器:int listen(int sockfd, int backlog);
客户端:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函数的第一个参数即为要监听的socket套接字,第二个参数为相应socket可以排队的最大连接个数。
connect函数的第一个参数即为客户端的socket套接字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。
(4)accept() 函数
**int accept(int sockfd, struct sockaddr addr, socklen_t addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。
accept函数返回的是已连接的socket套接字。
(5)read()、write() 等函数
调用网络I/O进行读写操作,即实现了网络中不同进程之间的通信,网络I/O操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
开发语言不同可能读写函数也就不同,只要把自己想要发送的消息,以字节流的方式写入Socket或者从Socket读出来即可实现网络的I/O操作;
(6)close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用close关闭打开的文件
1.3 Socket类中 bind() 和 std::bind() 冲突问题
Socket中有一个bind函数, 原型如下 :
// socket.h
int bind(int, const struct sockaddr *, socklen_t)
该函数是绑定Socket,而在C++11的 thread 库中增加了std::bind(_Fp &&__f, _BoundArgs &&__bound_args…) 函数,可以用该函数绑定函数指针,当在C++类中添加了using namespace std; 之后,调用socket.h中的 bind 方法则会出现问题,使得bind函数调用不是你想要的行为。
解决方案:
1、在使用 socket.h 的 bind() 函数的类中不要使用using namespace std,即通过namespace来回避同名函数问题;
2、在创建多线程时,CreateThread() 是用的Windows API来创建的线程,而thread库是c++的线程库,可以跨平台。因此当在win32平台下,可以回避 thread 库里的std::bind()函数;
1.4 c++ 代码
(1)多个client端可同时访问server端,每个client端对应一个独立线程,若涉及到共享数据时,可使用std::lock_guard< std::mutex > m_locker(mtx) 来进行加锁保护;
(2)因代码运行在同台电脑上,IP地址设为127.0.0.1;
server端代码如下:
// Summary: Socket通信;
// Author: jryang
// email: muyijames@126.com
// Date: 2020-09-10
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<thread>
#include<winsock2.h> //引用头文件
#pragma comment(lib,"ws2_32.lib") //链接库文件
//using namespace std;
char Ip[20][200] = { '\0' };
int iConnect = 0; //当前客户端数量
std::string function1(){
std::cout << "running function1..." << std::endl;
_sleep(5);
std::string temp;
temp = "function1 call back...";
return temp;
}
std::string function2(){
std::cout << "running function1..." << std::endl;
std::string temp;
temp = "function2 call back...";
return temp;
}
//使用thread库时,不要使用namespace std,因为std::bind()有冲突;
void test_threadpro(void* pParam) //创建多线程函数;
{
std::cout << "a new client connected..." << std::endl;
SOCKET hsock = (SOCKET)pParam;
char buffer[1024];
char sendBuffer[1024];
if (hsock != INVALID_SOCKET) //INVALID_SOCKET表示无效
std::cout << "Start receive information from IP:" << Ip[iConnect] << std::endl << std::endl;
while (true){
int num = recv(hsock, buffer, 1024, 0); //阻塞函数,等待接受内容
if (num <= 0){
std::cout << "Client with IP:" << Ip[iConnect] << " disconnected!" << std::endl;
break;
}
if (num >= 0)
std::cout << "Information from:" << Ip[iConnect] << ":" << buffer << std::endl;
if (!strcmp(buffer, "function1")){
memset(sendBuffer, 0, 1024);
std::string temp;
temp = function1();
strcpy(sendBuffer, temp.c_str());
std::cout << "sleep 5s..." << std::endl;
_sleep(5000);
int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0); //回送信息
std::cout << "The message sent to IP:" << Ip[iConnect] << "is: " << sendBuffer << std::endl;
}
else if (!strcmp(buffer, "function2")){
memset(sendBuffer, 0, 1024);
std::string temp;
temp = function2();
strcpy(sendBuffer, temp.c_str());
int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0); //回送信息
std::cout << "The message sent to IP:" << Ip[iConnect] << "is: " << sendBuffer << std::endl;
}
else if (!strcmp(buffer, "exit")){
//如果接受到 exit 结束该进程
std::cout << "Client with IP:" << Ip[iConnect] << " disconnected!" << std::endl;
std::cout << "Server Process Close: " << std::endl;
return;
}
else{
memset(sendBuffer, 0, 1024);
strcpy(sendBuffer, "Waiting for development...");
int ires = send(hsock, sendBuffer, sizeof(sendBuffer), 0);
std::cout << "The message sent to IP:" << Ip[iConnect] << "is: " << sendBuffer << std::endl;
}
}
return;
};
int main(void)
{
WSADATA wsd; //定义WSADATA对象
if (WSAStartup(MAKEWORD(2, 2), &wsd)){
printf("Initlalization Error!");
return -1;
}
SOCKET m_SockServer; //创建socket对象
sockaddr_in serveraddr; //创建sockaddr_in对象储存自身信息(当有多个端口,可以多个绑定)
sockaddr_in serveraddrfrom;
SOCKET m_Server[20]; //创建socket数组来存放来自客户端的信息最大连接数为20
serveraddr.sin_family = AF_INET; //设置服务器地址家族
serveraddr.sin_port = htons(4600); //设置服务器端口号
serveraddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
m_SockServer = socket(AF_INET, SOCK_STREAM, 0); //创建一个临时变量并赋值给m_SockServer
int i = bind(m_SockServer, (sockaddr*)&serveraddr, sizeof(serveraddr)); //把名字和套接字绑定
std::cout << "bind:" << i << std::endl;
int iMaxConnect = 20; //最大连接数
int iLisRet;
char buf[] = "THIS IS SERVER\0";
char WarnBuf[] = "It,is voer Max connect\0";
int len = sizeof(serveraddr); //serveraddr所占的字节大小
while (true){
iLisRet = listen(m_SockServer, 0); //进行监听
int temp = 0;
int Len = sizeof(serveraddrfrom);
m_Server[iConnect] = accept(m_SockServer, (sockaddr*)&serveraddrfrom, &len);
if (m_Server[iConnect] != INVALID_SOCKET) //INVALID_SOCKET表示无效
{
if (getsockname(m_Server[iConnect], (struct sockaddr*)&serveraddrfrom, &Len) != -1)
{
printf("listen address = %s:%d\n", inet_ntoa(serveraddrfrom.sin_addr), ntohs(serveraddrfrom.sin_port));
sprintf(Ip[iConnect], "%s", inet_ntoa(serveraddrfrom.sin_addr));
}
else{
printf("getsockname error\n");
exit(0);
}
int ires = send(m_Server[iConnect], buf, sizeof(buf), 0); //发送字符过去
std::cout << "accept" << ires << std::endl << std::endl; //显示已经连接次数
iConnect++;
if (iConnect > iMaxConnect){
//判断连接数是否大于最大连接数
int ires = send(m_Server[iConnect], WarnBuf, sizeof(WarnBuf), 0);
}
else{
std::thread th1(test_threadpro,(void*)m_Server[--iConnect]); //启动线程
th1.detach();
}
}
}
WSACleanup(); //用于释放ws2_32.dll动态链接库初始化时分配的资源
}
client端代码如下:
// Summary: Socket通信;
// Author: jryang
// email: muyijames@126.com
// Date: 2020-09-10
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<cstdlib>
#include<time.h>
#include<winsock2.h> //引用头文件
#pragma comment(lib,"ws2_32.lib") //链接库文件
using namespace std;
int main(void)
{
WSADATA wsd; //定义WSADATA对象
WSAStartup(MAKEWORD(2, 2), &wsd);
SOCKET m_SockClient;
sockaddr_in serverAddr; //服务器信息
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(4600);
serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
m_SockClient = socket(AF_INET, SOCK_STREAM, 0);
int i = connect(m_SockClient, (sockaddr*)&serverAddr, sizeof(serverAddr));
cout << "Connection status " << i << endl;
char buffer[1024];
char inBuf[1024];
int num;
num = recv(m_SockClient, buffer, 1024, 0); //阻塞函数,等待接受内容
if (num > 0){
cout << "Receive form server---" << buffer << endl;
while (true){
num = 0;
cin >> inBuf;
if (!strcmp(inBuf, "exit")) //如果输入的是exit则断开连接
{
send(m_SockClient, inBuf, sizeof(inBuf), 0);
return 0;
}
send(m_SockClient, inBuf, sizeof(inBuf), 0);
num = recv(m_SockClient, buffer, 1024, 0);
if (num >= 0)
cout << "Receive form server: " << buffer << endl; //输出接受到的内容
}
}
}
有错误之处请多多指教哈!!!