注:本内容的相关代码除 pthread 和 QT 外,仅针对 Socket 在 windows系统 下的通讯

Socket概述

基于TCP/IP协议栈的网络编程是最经典的网络编程方式。

主要是使用各种编程语言,利用操作系统提供的套接字网络编程接口,直接开发各种网络应用程序。

本文章主要讲解这种网络编程的相关技术。

套接字Socket: {IP : Port},如 192.168.0.1 : 8080

Socket作为一种网络通讯的应用程序编程接口(API),依赖于操作系统和编程语言,常见的Socket API实现有:

  • Unix(Linux):Berkeley Socket

  • Windows:WinSock

常用的Socket类型

  • 流式Socket: SOCK_STREAM → TCP

  • 数据报式Socket:SOCK_DGRAM → UDP

  • 原始Socket: SOCK_RAW → IP

一般情况下,不推荐使用 SOCK_RAW,其基于原始 IP,许多完善的类似于 TCP的协议全部需要自己编写

常见的通讯方式

C/S模式是因特网上应用程序最常用的通信模式,即:客户端与服务器模式(Client&Server)

工作流程大致如下:

c/s

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 为例)

codeblocks-option1

有些编译器,可以通过如下代码解决,建议都试试:

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];
}
/*
sin_family一般为AF_INET,表示使用IP地址族;
sin_port是以网络字节序表示的16位端口号;
sin_addr是网络字节序的32位IP地址;
sin_zero字段一般不用,用0填充
*/

常用操作&函数

初始化

1
2
3
4
5
6
7
WORD SocketVersion = MAKEWORD(2,2); //创建2.2版本的socket,数据类型WORD
WSADATA wsd; //WSADATA是函数WSAStartup()所返回的socket数据类型

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 //服务器端的地址长度
);
//连接失败 返回 SOCKET_ERROR

服务器:与IP地址绑定

1
2
3
4
5
6
//函数定义
int bind(SOCKET sv, //标识一个未绑定的套接口描述字
const struct sockaddr* name,//存储了套接口的地址信息
int namelen //地址参数(name)的长度
);
//连接失败 返回 SOCKET_ERROR

服务器:监听

1
2
3
//函数定义
int listen( SOCKET sv,int backlog);
//连接失败 返回 SOCKET_ERROR
  • sv参数:代表一个已绑定了地址和端口,但还未建立连接的套接口描述字(服务器)

  • Backlog参数:指定了正在等待连接的最大队列长度

服务器:接收客户端连接请求

1
2
3
4
5
6
7
//函数定义
SOCKET accept(SOCKET sv, // s标识一个套接字
struct sockaddr* addr, //存放发出连接请求的客户机的地址信息
int * addrlen //指出客户套接口地址结构的长度
);
//失败 返回 INVALID_SOCKET,是一个SOCKET类型的数据
//成功 返回连接成功的那个客户端实体 SOCKET类型

Recv()函数

用于接收已连接好的c/s发送的信息

1
2
3
4
5
6
7
//函数定义
int recv(SOCKET s, //已建立连接的套接口
char * buf, //为用于接收数据的缓冲区
int len, //为缓冲区的长度
int flags //指定调用的方式
);
//失败 返回 SOCKET_ERROR

Send()函数

用于对已连接的c/s发送信息

1
2
3
4
5
6
7
//函数定义
int send (SOCKET s, //用于标识已建立连接的套接字
const char* buf, //是一个字符缓冲区,内有将要发送的数据
int len, //即将发送的缓冲区中的字符数
int flags //用于控制数据传输方式
);
//失败 返回 SOCKET_ERROR

封装

为了之后更好更快捷的使用socket进行编程,这里我们可以用C++的面向对象的特性将socket封装成类,保存在一个头文件中。

引入头文件

1
#include<winsock2.h>

初始化WSA

1
2
3
4
5
6
7
8
9
10
11
//初始化Sokcet
bool InitMySock(void){
WORD SocketVersion = MAKEWORD(2,2); //创建2.2版本的socket,数据类型WORD
WSADATA wsd; //WSADATA是函数WSAStartup()所返回的socket数据类型
//初始化
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;//socket实例
SOCKADDR_IN ServerAddr;//服务器自己的IP信息
public:
bool CreateSv(void){
//创建socket
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){
//配置:将传入的IP和端口写入ServerAddr中
ServerAddr.sin_family = AF_INET;
//ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
ServerAddr.sin_addr.s_addr = inet_addr(IPaddr);
ServerAddr.sin_port = htons(PORT);
}
bool Bind(void){
//与自己的IP信息进行绑定
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){
//监听,上限由传入的c变量决定
RetVal = listen(sv,c);
if (RetVal == SOCKET_ERROR){
cout << "Socket Listen Failed!" << endl;
return false;
}
return true;
}

MyClient AcptClt(void){
//与客户端建立连接 ,成功后返回该client
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;//自己的ID标识
char name[80];//客户端昵称
int RetVal;//检错变量
SOCKET ct;//socket实例
SOCKADDR_IN ServerAddr;//要连接的服务器的地址信息
SOCKADDR_IN ClientAddr;//自己的地址信息
public:
bool CreateCt(void){
//创建socket
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
#include <pthread.h>

之后,我们还需要添加线程函数进去:

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
//传递Message结构体
typedef struct ARGS{
int type;//判断是服务端还是客户端
int len;//客户端个数
int trgid;//目标客户端ID
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"

//#include<windows.h>
//#pragma comment(lib,"ws2_32")

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;

//最多连10个
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;

//为目标定义ID
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;

//创建客户端Socket
if(!client.CreateCt())
return NULL;

//事实上,这里配置的是client要连接的服务器的信息
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;
}
//接收自己的ID
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聊天

前面介绍的通信都是需要 服务器 作为载体实现的。而我们可能更加需要没有中心就能点对点的聊天交流模式。

对此的解决方案之一,则是一个程序即是服务器也是客户端,与另一个程序进行互连,服务器只负责接收消息,客户端只负责发送消息。如下图所示:

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"

//#include<windows.h>
//#pragma comment(lib,"ws2_32")

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;

//创建客户端Socket
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));//设置随机port

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函数”联系起来。

下面则是我绘制的具体思路:工程文件目录

QT的实现

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

QT效果展示

代码已上传至本人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