注:本内容的相关代码除 pthread 和 QT 外,仅针对 Socket 在 windows系统 下的通讯
Socket概述
基于TCP/IP协议栈的网络编程是最经典的网络编程方式。
主要是使用各种编程语言,利用操作系统提供的套接字网络编程接口,直接开发各种网络应用程序。
本文章主要讲解这种网络编程的相关技术。
套接字Socket: {IP : Port},如 192.168.0.1 : 8080
Socket作为一种网络通讯的应用程序编程接口(API),依赖于操作系统和编程语言,常见的Socket API实现有:
常用的Socket类型
一般情况下,不推荐使用 SOCK_RAW,其基于原始 IP,许多完善的类似于 TCP的协议全部需要自己编写
常见的通讯方式
C/S模式是因特网上应用程序最常用的通信模式,即:客户端与服务器模式(Client&Server)
工作流程大致如下:

WinSock2
Winsock2 SPI服务提供者接口建立在Windows开放系统架构WOSA之上,是Winsock系统组件提供的面向系统底层的编程接口。
Winsock系统组件向上面向用户应用程序提供一个标准的API接口;
向下在Winsock组件和Winsock服务提供者(比如TCP/IP协议栈)之间提供一个标准的SPI接口。
各种服务提供者是Windows支持的DLL,挂载在Winsock2 的Ws2_32.dll模块下。
摘自:百度百科
由此我们不难发现,通过winsock2,我们可以通过调用函数直接对底层进行一些普通的操作,这大大简化了我们的学习成本,也使得我们可以更专注于对于算法处理上和实现上的问题。
编译预设
上面提到winsock2的服务挂载在Ws2_32.dll模块下,因此编译不通过时,可以在IDE的编译设置内补上如下的link option:(这里 以 code::blocks 为例)

有些编译器,可以通过如下代码解决,建议都试试:
1
| #pragma comment(lib,"ws2_32")
|
或
1
| #pragma comment(lib,"ws2_32.lib")
|
常用数据类型
数据类型:sockaddr_in
sockaddr_in 是一个与指定协议有关的地址结构指针,存储了套接口的地址信息.
Winsock中使用sockaddr_in结构指定IP地址和端口信息
1 2 3 4 5 6 7 8 9 10 11 12
| struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }
|
常用操作&函数
初始化
1 2 3 4 5 6 7
| WORD SocketVersion = MAKEWORD(2,2); WSADATA wsd;
if(WSAStartup(SocketVersion,&wsd) != 0){ cout << "Init Windows Socket Failed!" << endl; return -1; }
|
创建套接口
1 2 3 4 5 6
| SOCKET ct; ct = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(ct == INVALID_SOCKET){ cout << "Create Socket Failed!" << endl; return -1; }
|
关于socket()函数:
1 2 3 4
| SOCKET socket( int af, int type, int protocol );
|
af参数:PF_INET(AF_INET), PF_INET6, PF_LOCAL, …
type参数: SOCK_STREAM, SOCK_DGRAM,SOCK_RAW
protocol参数: 0 (IPPROTO_TCP、IPPROTO_UDP)
关闭Socket
1 2
| closesocket(ct); WSACleanup();
|
客户端:与服务器连接
1 2 3 4 5 6
| int connect(SOCKET ct, const struct sockaddr* name, int namelen );
|
服务器:与IP地址绑定
1 2 3 4 5 6
| int bind(SOCKET sv, const struct sockaddr* name, int namelen );
|
服务器:监听
1 2 3
| int listen( SOCKET sv,int backlog);
|
服务器:接收客户端连接请求
1 2 3 4 5 6 7
| SOCKET accept(SOCKET sv, struct sockaddr* addr, int * addrlen );
|
Recv()函数
用于接收已连接好的c/s发送的信息
1 2 3 4 5 6 7
| int recv(SOCKET s, char * buf, int len, int flags );
|
Send()函数
用于对已连接的c/s发送信息
1 2 3 4 5 6 7
| int send (SOCKET s, const char* buf, int len, int flags );
|
封装
为了之后更好更快捷的使用socket进行编程,这里我们可以用C++的面向对象的特性将socket封装成类,保存在一个头文件中。
引入头文件
初始化WSA
1 2 3 4 5 6 7 8 9 10 11
| bool InitMySock(void){ WORD SocketVersion = MAKEWORD(2,2); WSADATA wsd; if(WSAStartup(SocketVersion,&wsd) != 0){ cout << "Init Windows Socket Failed!" << endl; return false; } return true; }
|
服务器对象
基础操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class MyServer{ private: int RetVal; SOCKET sv; SOCKADDR_IN ServerAddr; public: bool CreateSv(void){ sv = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(sv == INVALID_SOCKET){ cout << "Create Socket Failed!" << endl; return false; } return true; } void SockClose(void){ closesocket(sv); }
|
绑定Bind信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public: void Config(char *IPaddr,unsigned short PORT){ ServerAddr.sin_family = AF_INET; ServerAddr.sin_addr.s_addr = inet_addr(IPaddr); ServerAddr.sin_port = htons(PORT); } bool Bind(void){ RetVal = bind(sv, (SOCKADDR *)&ServerAddr, sizeof(SOCKADDR_IN)); if(RetVal == SOCKET_ERROR){ cout << "Socket Bind Failed!" << endl; return false; } return true; }
|
监听&接收客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public: bool Listen(int c){ RetVal = listen(sv,c); if (RetVal == SOCKET_ERROR){ cout << "Socket Listen Failed!" << endl; return false; } return true; }
MyClient AcptClt(void){ MyClient client; SOCKADDR_IN ClientAddr; int ClientAddrLen = sizeof(ClientAddr); client.ct = accept(sv,(SOCKADDR*)&ClientAddr,&ClientAddrLen); if(client.ct == INVALID_SOCKET){ cout << "Accept Clients Failed!" << endl; }else{ cout << "Accept Clients Succeed!" << endl; client.ClientAddr = ClientAddr; } return client; }
|
客户端对象
与服务器对象类似地,我们也可以创建客户端的class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| class MyClient{ private: int id; char name[80]; int RetVal; SOCKET ct; SOCKADDR_IN ServerAddr; SOCKADDR_IN ClientAddr; public: bool CreateCt(void){ ct = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(ct == INVALID_SOCKET){ cout << "Create Socket Failed!" << endl; return false; } return true; } void SockClose(void){ closesocket(ct); } void Config(char *IPaddr,unsigned short PORT){ ServerAddr.sin_family = AF_INET; ServerAddr.sin_addr.s_addr = inet_addr(IPaddr); ServerAddr.sin_port = htons(PORT); } bool CntServ(void){ RetVal = connect(ct,(SOCKADDR*)&ServerAddr,sizeof(SOCKADDR_IN)); if(RetVal == SOCKET_ERROR){ cout << "Connect Server Failed!" << endl; return false; }else{ cout << "Connect Succeed!" << endl; return true; } } void SetID(int tid){ id = tid; } };
|
多线程
根据我们封装好的类,和前面通讯方式的图,再在recv和send中使用while循环,我们便可以实现简单的socket的通讯了!具体不再展示。
但是,不难发现,上面完成的工作,还是不能达到预期要求。Recv和Send的调用是存在先后顺序的,换句话说,recv和send存在阻塞问题。
为了解决这个问题,就不得不使用多线程的方法来使得send与recv同时进行,互不干扰。
这里,通过pthread库来实现此功能。
更多pthread的知识,移步另一篇文章:Pthread库|C/C++多线程
将之前封装的类保存为MySocket.h作为头文件。
引入头文件
(注:Windows系统下想要使用pthread库需要安装pthread-win32,详见上面这篇文章或通过目录跳到推荐资料处,已给出超链接)
之后,我们还需要添加线程函数进去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void *RecvMsg(void* args){ int RetVal; MyClient *client = (MyClient*)*args; char RecvBuff[BUFSIZ]; printf("<Recieve Thread Open>\n"); while(1){ ZeroMemory(RecvBuff,BUFSIZ); RetVal = recv(client->ct,RecvBuff,BUFSIZ,0); if(RetVal == SOCKET_ERROR){ cout << "Receive Messages Failed!" << endl; break; } cout << RecvBuff+2 << endl; } pthread_exit(NULL); return NULL; }
|
于是在服务器端的主函数中,通过while循环来开线程。逻辑是:
① 监听
② 如果监听成功,且接收到一个客户端,将其存储在MyClient*类型的args中
③ 开Recv线程,传入&args给线程函数
为了实现服务器的“一对多”通信,最好的方法是建立MyClient*类型的链表存储已连接的客户端,同时以达到对目标客户端的单发送,而非广播。
因此,主函数仅需循环一个send函数即可。
如下图所示:

在前面的class定义中,我对MyClient创建了一些私有属性:id和name。因此,我们还可以对Recv和Send的的处理进行更近一步的优化。
比如:让客户端知道服务器给自己设置的ID,让服务器知道客户端自定义的名字。
处理起来也很简单,只需在二者真正进行“通信交流”前,以单工的方式互换信息,并保存下来即可。
甚至可以直接让服务器每次都是广播发送,但是发送的内容前必须添加一个头部信息,比如是目标客户端的ID,然后客户端收到服务器的信息之后,先解析头部,如果目标ID不是自己,则丢弃。
那么,传入线程的args就不能单单只是MyClient对象了。需要再建立一个结构体来解决:
1 2 3 4 5 6 7
| typedef struct ARGS{ int type; int len; int trgid; MyClient* Allcts; }Args,*pArgs;
|
然后再更改一下Recv线程的内部处理逻辑即可。
这竟然还有点 “计算机网络” 内味了(笑)~
全双工通信
通过以上的封装,我们已然对即将编写的代码有了思路。接下来就着手实现基于TCP的Socket通信吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
| #include<iostream> #include<cstdio> #include<pthread.h> #include"MySocket.h"
using namespace std;
void *Asv(void*){ pthread_t tdsend[8],tdrecv[8]; MyServer server; int RetVal; char ip[100]; unsigned short port; cout << "Server Thread activated." << endl; cout << endl; cout << "Enter IP address:"; cin >> ip; cout << "Enter Port:"; cin >> port; cout << endl;
static int i = 0; if(!server.CreateSv()) return NULL;
server.Config(ip,port); cout << "Config Succeed,waiting……" << endl; if(!server.Bind()) return NULL;
MyClient clients[10]; pArgs args = (pArgs)malloc(sizeof(Args)); strcpy(args->MyName,"Server"); args->Allcts = clients; args->type = 1; while(1){
if(server.Listen(10)) { clients[i] = server.AcptClt(); if(clients[i].ct == INVALID_SOCKET) continue;
char id[5]; sprintf(id,"%d",i+1); send(clients[i].ct,id,5,0); args->len = i; struct in_addr addr; memcpy(&addr, &(clients[i].ClientAddr.sin_addr.s_addr), 4); cout << "--------------------------------------------" << endl; cout << "用户已连接:ID IP PORT" << endl; cout << " " << id << " " << inet_ntoa(addr) << " " << clients[i].ClientAddr.sin_port << endl; cout << "--------------------------------------------" << endl; RetVal = pthread_create(&tdrecv[i],NULL,RecvMsg,(void*)&args); if (RetVal){ cout << "Error:unable to create recieve messages thread." << RetVal << endl; exit(-1); } i++; }else{break;} RetVal = pthread_create(&tdsend[i],NULL,SendMsg,(void*)&args); if (RetVal){ cout << "Error:unable to create send messages thread." << RetVal << endl; exit(-1); } } server.SockClose(); return NULL; }
void *Act(void*){ cout << "Server Thread activated." << endl; int RetVal; unsigned short port; pthread_t tdsend,tdrecv; MyClient client; char name[100]; char ip[100]; cout << endl; cout << "Enter your name:"; cin >> name; strcpy(client.name,name); if(!InitMySock()) return NULL;
if(!client.CreateCt()) return NULL;
cout << endl; cout << "Enter Server's IP address:"; cin >> ip; cout << "Enter Server's Port:"; cin >> port; cout << endl; client.Config(ip,port);
if(!client.CntServ()){ return NULL; } char id[5]; recv(client.ct,id,5,0); sscanf(id,"%d",&client.id); pArgs args = (pArgs)malloc(sizeof(Args)); args->len = 0; args->Allcts = &client; args->trgid = client.id; args->type = 0; strcpy(args->MyName,client.name);
RetVal = pthread_create(&tdsend,NULL,SendMsg,(void*)&args); if (RetVal){ cout << "Error:unable to create send messages thread:" << RetVal << endl; exit(-1); } RetVal = pthread_create(&tdrecv,NULL,RecvMsg,(void*)&args); if (RetVal){ cout << "Error:unable to create recieve messages thread:" << RetVal << endl; exit(-1); } pthread_join(tdsend,NULL); pthread_join(tdrecv,NULL); return NULL; }
int main(int args,char *argv[]){
int RetVal; pthread_t tds,tdc; if(!InitMySock()) return -1; int user_option; cout << "----------Socket通讯程序-------------------" << endl; cout << "1.作为服务器使用" << endl; cout << "2.作为客户端使用" << endl; cout << "请选择:";cin >> user_option; cout << "-------------------------------------------" << endl; switch(user_option){ case 1: RetVal = pthread_create(&tds,NULL,Asv,NULL); if (RetVal){ cout << "Error:unable to create server thread." << RetVal << endl; exit(-1); } pthread_join(tds,NULL); break; case 2: RetVal = pthread_create(&tdc,NULL,Act,NULL); if (RetVal){ cout << "Error:unable to create client thread." << RetVal << endl; exit(-1); } pthread_join(tdc,NULL); break; default: break; } return 0; }
|
p2p聊天
前面介绍的通信都是需要 服务器 作为载体实现的。而我们可能更加需要没有中心就能点对点的聊天交流模式。
对此的解决方案之一,则是一个程序即是服务器也是客户端,与另一个程序进行互连,服务器只负责接收消息,客户端只负责发送消息。如下图所示:

话不多说,直接贴代码
(注:此代码中的服务器不再是“一对多”类型,可自己设置while循环改进)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
| #include<iostream> #include<cstdio> #include<pthread.h> #include<time.h> #include"MySocket.h"
using namespace std;
void *Act(void*){ cout << "Client Thread activated." << endl; int RetVal; char name[100]; char ip[100] = "127.0.0.1"; unsigned short port; MyClient client; pthread_t tdsend,tdrecv; cout << endl; cout << "Enter your name:" << endl; cin >> name; if(!InitMySock()) return NULL;
if(!client.CreateCt()) return NULL;
cout << endl; cout << "Enter Server's IP address:"; cin >> ip; cout << "Enter Server's Port:"; cin >> port; cout << endl;
client.Config(ip,port);
if(!client.CntServ()){ return NULL; } char OK[] = "YES"; send(client.ct,OK,8,0);
pArgs args = (pArgs)malloc(sizeof(Args)); args->len = 0; args->Allcts = &client; args->type = 2; RetVal = pthread_create(&tdsend,NULL,SendMsg,(void*)&args); if (RetVal){ cout << "Error:unable to create send messages thread:" << RetVal << endl; exit(-1); } pthread_join(tdsend,NULL); client.SockClose(); WSACleanup(); return NULL; }
int main(int args,char *argv[]){ int RetVal; char ip[100] = "127.0.0.1"; unsigned short port = = 1024+rand()%(65535-1024); MyServer server; pthread_t tdrecv,tdc; if(!InitMySock()) return -1;
srand(time(0)); cout << "Server Thread activated." << endl; cout <<"-----------------------------------" <<endl; cout << "IP:127.0.0.1" << endl; cout << "port:" << port << endl; cout <<"-----------------------------------" <<endl;
if(!server.CreateSv()) return NULL;
server.Config(ip,port);
if(!server.Bind()) return NULL; if(!server.Listen(10)) { return NULL; } RetVal = pthread_create(&tdc,NULL,Act,NULL); if (RetVal){ cout << "Error:unable to create client thread:" << RetVal << endl; exit(-1); } MyClient client; client = server.AcptClt(); if(client.ct == INVALID_SOCKET) return NULL;
cout << "wait your friend connect...." << endl; char OK[8]; while(strcmp(OK,"YES") != 0){ ZeroMemory(OK,8); RetVal = recv(client.ct,OK,8,0); } cout << "link OK!" << endl; pArgs args = (pArgs)malloc(sizeof(Args)); args->len = 0; args->Allcts = &client; args->type = 2; RetVal = pthread_create(&tdrecv,NULL,RecvMsg,(void*)&args); if (RetVal){ cout << "Error:unable to create recieve messages thread:" << RetVal << endl; exit(-1); } pthread_join(tdrecv,NULL); server.SockClose();
pthread_join(tdc,NULL); WSACleanup(); return 0; }
|
窗口化实现
虽然大多数问题得以解决了,但是在实际运行中,还是出现了许多问题。
比如:输入信息还没有发送时,对方发送的信息被打印出来,从而紧接着我们的输入内容显示在命令行窗口上。功能没有受到限制,但是美观度上却不尽人意。因此,不得不使用“窗口化编程”来解决。
这里,我选用的是C++中,比较出名的QT框架。(关于QT可移步至 文章:QT|C++GUI库)
好在QT中,已经内置有“QTcpsocket”模块,也是已经为用户封装好socket的常用函数,而且“QThread”模块也提供了多线程的使用。
因此,可以免去很多前期操作,而把重心放在如何将“文本框与recv函数”、“发送按钮与send函数”联系起来。
下面则是我绘制的具体思路:

终于,功夫不负有心人,还是得到了还算满意的成果:

代码已上传至本人GitHub仓库中,欢迎查看~~~
参考&推荐 资料
1.addrSrv.sin_addr和addrSrv.sin_addr.S_un.S_addr的区别
2.C++的pthread使用|简书
3.Socket网络编程实现过程简单总结|简书
4.C++库中的sprintf与sscanf
5.QT基础入门|bilibili
6.在windows下配置pthread