传输技术-SOCKET通信

本文最后更新于:2021-08-09 晚上

SOCKET通信

socket也被叫做“套接字”,应用程序通常通过“套接字”向网络发出请求或者接收请求。socket表示是:IP地址加上端口号,如127.0.0.1:8080。Socket编程有两种通信协议可以选择,一种是TCP,另一种是UDP。先来说一下基于TCP的Socket编程。

基于TCP Socket编程

TCP是一种基于连接的协议,在进行通信之前,必须要建立连接,其中服务端监听请求,客户端发送请求。当建立好了连接之后,就可以开始通信了。

函数介绍
Socket

根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所有资源的函数。

1
2
3
4
SOCKET socket (
_In_ int af,
_In_ int type,
_In_ int protocol);

af:指定地址族规范。地址系列的可能值在Winsock2.h头文件中定义。当前支持AF_INET或者AF_INET6,它们是IPV4和IPV6的互联网地址族格式。

type:指定socket类型,SOCK_STREAM类型指定产生流式套接字,SOCK_DGRAM类型指定产生数据报式套接字,而SOCK_RAW类型指定产生原始套接字(只有管理员权限的用户可以创建原始套接字)。

protocol:与特定地址家族相关的协议IPPROTO_TCP、IPPROTO_UP、IPPROTO_IP,如果指定为0,那么系统会根据地址格式和套接字类别,自动选择一个合适的协议。

返回值:如果没有发生错误,则套接字返回引用新套接字的描述符,否则返回INVALID_SOCKET。

bind

将本地地址与套接字相关联

1
2
3
4
5
int bind(
SOCKET s,
const sockaddr *addr,
int namelen
);

s:标识未绑定套接字的描述符。

addr:指向要分配给绑定套接字的本地地址的sockaddr结构的指针。

namelen:name参数指向值的长度。

返回值:如果没有发生错误,则bind返回0,否则返回SOCKET_ERROR。

htons

将整型变量从主机字节顺序转变成网络字节顺序,就是整数在地址空间中的存储方式变为高位字节存放在内存的低地址处。

1
2
3
u_short htons(
u_short hostshort
);

hostshort:指定主机字节顺序为16位。

返回值:返回TCP/IP网络字节顺序。

inet_addr

将一个点分十进制的IP转换成一个长整型数。

1
2
3
unsigned long inet_addr(
const char *cp
);

返回值:如果没有发生错误,返回一个无符号长整型值,其中包含给定互联网地址的适当的二进制表示形式。

listen函数

将一个套接字置于正在监听传入连接的状态。

1
2
3
int listen (
_In_ SOCKET s,
_In_ int backlog);

s:标识绑定的未连接套接字的描述符。

backlog:指定待连接队列的最大长度。如果设置为SOMAXCONN,则负责套接字的底层服务提供商将积压设置为最大合理值。如果设置为SOMAXCONN_HINT(N)(其中N是数字),则积压值为N,调整范围(200,65535)。

返回值:如没有发生错误,返回0,否则返回SOCKET_ERROR。

accept

允许在套接字上尝试连接

1
2
3
4
SOCKET accept (
_In_ SOCKET s,
_Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR *addr,
_Inout_opt_ int FAR *addrlen);

s:描述符,用于标识使用listen功能并处于侦听状态的套接字。连接实际上是由accept返回的套接字。

addr:指定一个可选缓冲区的指针,它接受通信层中已知连接实体的地址。addr参数的确切格式由创建sockaddr结构的套接字时建立的地址族来确定。

addrlen:指向一个整数的可选指针,其中包含由addr参数指向的结构长度。

返回值:如果没有发生错误,则accept返回一个SOCKET类型的值,该值是新套接字的描述符。此返回值是实际连接所在的套接字的句柄。否则返回INVALID_SOCKET。

send

在建立连接的套接字上发送数据

1
2
3
4
5
int send (
_In_ SOCKET s,
_In_reads_bytes_(len) const char FAR * buf,
_In_ int len,
_In_ int flags);

s:标识连接的套接字的描述符。

buf:指向要发送的数据缓冲区的指针。

len:由buf参数指向缓冲区中数据的长度。

flags:指定一组调用方式的标志,一般置为0。

返回值:如果没有发生错误,返回发送的字节数,否则返回SOCKET_ERROR。

recv

从连接的套接字或绑定的无连接套接字中接收数据。

1
2
3
4
5
int recv (
_In_ SOCKET s,
_Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf,
_In_ int len,
_In_ int flags);

s:标识连接的套接字的描述符。

buf:指向缓冲区的指针,用于接收传入的数据。

len:由buf参数指向缓冲区中数据的长度。

flags:指定一组调用方式的标志,一般置为0。

返回值,如果没有发生错误,则recv返回接收到的字节数,由buf参数指向的缓冲区将包含接收到的数据。如果连接已经正常关闭,则返回值为0.

实现原理

客户端先初始化winsock环境,然后调用Socket函数创建套接字,然后进行结构体的设置,调用bind函数绑定,再调用listen函数进行监听。当有连接请求时,调用accept函数接收连接请求。建立连接后,使用recv和send函数进行通信。

服务端先初始化winsock环境,然后调用Socket函数创建套接字,然后进行结构体的设置,然后调用connect函数发送连接请求,建立连接后,使用recv和send函数进行通信。

服务端代码:
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
#include <Windows.h>
#include <stdio.h>
//#include <WinSock2.h>
#pragma comment(lib, "Ws2_32.lib")
BOOL SocketBindAndListen(char* lpszIP, int Port);
void AcceptRecvMsg();
void SendMsg(char* pszSend);
UINT RecvThreadProc(LPVOID lpVoid);
SOCKET g_ServerSocket;
SOCKET g_ClientSocket;
int main() {

if (SocketBindAndListen("IP地址", 12345) == FALSE)
{
printf("建立连接失败\n");
return -1;
}
printf("连接建立成功,开始通信\n");
char szSendmsg[MAX_PATH] = { 0 };
while (1)
{
gets_s(szSendmsg);
SendMsg(szSendmsg);
}
return 0;
}
BOOL SocketBindAndListen(char* lpszIP, int Port)
{
//初始化winsock库
WSADATA wsaData = { 0 };
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
g_ServerSocket = socket(AF_INET, SOCK_STREAM, 0);

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(Port);
addr.sin_addr.S_un.S_addr = inet_addr(lpszIP);
//绑定IP和端口
if (bind(g_ServerSocket, (sockaddr*)(&addr), sizeof(addr)) == SOCKET_ERROR)
{
return FALSE;
}
//设置监听
if (listen(g_ServerSocket, 1) == SOCKET_ERROR)
{
return FALSE;
}
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL);
return TRUE;
}

void AcceptRecvMsg()
{
sockaddr_in addr = { 0 };
int len = sizeof(addr);
g_ClientSocket = accept(g_ServerSocket, (sockaddr*)(&addr), &len);
printf("接收客户端连接请求\n");
char szBuffer[MAX_PATH] = { 0 };
while (1)
{
//接收数据
int Ret = recv(g_ClientSocket, szBuffer, MAX_PATH, 0);
if (Ret <= 0)
{
continue;
}

printf("接收到数据:%s\n", szBuffer);
}
}

void SendMsg(char* pszSend)
{
send(g_ClientSocket, pszSend, (1 + strlen(pszSend)), 0);
printf("发送数据:%s\n", pszSend);
}
UINT RecvThreadProc(LPVOID lpVoid) {
AcceptRecvMsg();
return 0;
}
客户端代码:
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
#include <Windows.h>
#include <stdio.h>
//#include <WinSock2.h>
#pragma comment(lib, "Ws2_32.lib")
SOCKET g_ServerSocket;
BOOL Connection(char* lpszServerIP, int ServerPort);
void SendMsg(char* pszSend);
void RecvMsg();
UINT RecvThreadProc(LPVOID lpVoid);
int main()
{
printf("请输入服务器IP\n");
char ipaddr[32] = { 0 };
gets_s(ipaddr);
if (Connection(ipaddr, 12345) == FALSE)
{
printf("建立连接失败\n");
getchar();
return -1;
}
printf("建立连接成功,开始通信\n");
char szSend[MAX_PATH] = { 0 };
while (1) {
gets_s(szSend);
SendMsg(szSend);
}
return 0;
}

BOOL Connection(char* lpszServerIP, int ServerPort)
{
WSADATA wsaData = { 0 };
WSAStartup(MAKEWORD(2, 2), &wsaData);
g_ServerSocket = socket(AF_INET, SOCK_STREAM, 0);
if (g_ServerSocket == INVALID_SOCKET)
{
return FALSE;
}
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_port = htons(ServerPort);
addr.sin_addr.S_un.S_addr = inet_addr(lpszServerIP);

if (connect(g_ServerSocket, (sockaddr*)(&addr), sizeof(addr)))
{
return FALSE;
}
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)RecvThreadProc, NULL, NULL, NULL);
return TRUE;
}
void SendMsg(char* pszSend) {
send(g_ServerSocket, pszSend, (1 + strlen(pszSend)), 0);
printf("发送消息:%s\n",pszSend);
}
void RecvMsg() {
char szBuffer[MAX_PATH] = { 0 };
while (1)
{
int ret = recv(g_ServerSocket, szBuffer, MAX_PATH, 0);
if (ret <= 0)
{
continue;
}
printf("接收到消息:%s\n", szBuffer);
}
}


UINT RecvThreadProc(LPVOID lpVoid) {
RecvMsg();
return 0;
}
测试

服务端

客户端

基于UDP Socket编程

函数介绍
sendto

将数据发送到特定的目的地

1
2
3
4
5
6
7
8
int sendto(
SOCKET s,
const char *buf,
int len,
int flags,
const sockaddr *to,
int tolen
);

s:标识套接字的描述符。

buf:指向要发送的数据缓冲区的指针。

len:由buf参数指向的数据长度。

flags:指定一组调用方式的标志,一般为0。

to:指向包含目标套接字地址的sockaddr结构的可选指针。

tolen:由to参数指向的地址的大小。

返回值:如果没有发生错误,返回发送的总字节数,否则返回SOCKET_ERROR。

recvfrom

接收数据报并存储源地址

1
2
3
4
5
6
7
8
int recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from,
int *fromlen
);

s:标识套接字的描述符。

buf:指定传入数据的缓冲区。

len:由buf参数指向的数据长度。

flags:指定一组调用方式的标志,一般为0。

from:指向sockaddr结构中的缓冲区的可选指针,它将在返回时保存源地址。

fromlen:由from参数指向的地址的大小。

返回值:如果没有发生错误,返回接收到的总字节数,否则返回SOCKET_ERROR。

实现原理

UDP的通信框架比起TCP更加简单,UDP是基于无连接的通信,它可以直接使用sendto和recvfrom函数进行数据的发送与接收。

服务端:
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
BOOL bind() {
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
serSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (serSocket == INVALID_SOCKET)
{
printf("socket 错误\n");
return 0;
}
sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(12345);
serAddr.sin_addr.S_un.S_addr = inet_addr("IP地址");
if (bind(serSocket, (sockaddr*)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
{
printf("绑定失败\n");
closesocket(serSocket);
return 0;
}
return 1;
}
int main()
{
if (bind() == FALSE)
{
printf("wrong");
return -1;
}
printf("开始通信\n");
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
while (true)
{
char recvData[MAX_PATH];
int ret = recvfrom(serSocket, recvData, MAX_PATH, 0, (sockaddr*)&remoteAddr, &nAddrLen);
if (ret > 0)
{
recvData[ret] = 0x00;
printf("接收到信息:%s\n",recvData);
}


char sendData[MAX_PATH] = { 0 };
gets_s(sendData);
printf("发送数据:%s\n", sendData);
sendto(serSocket, sendData, strlen(sendData), 0, (sockaddr*)&remoteAddr, nAddrLen);

}
return 0;
}
服务端:
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
BOOL bind() {
WORD socketVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (WSAStartup(socketVersion, &wsaData) != 0)
{
return 0;
}
sclient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
return 1;
}
int main()
{
if (bind() == FALSE)
{
printf("wrong\n");
return -1;
}
printf("开始通信\n");
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(12345);
sin.sin_addr.S_un.S_addr = inet_addr("IP地址");
int len = sizeof(sin);
while (1)
{

char sendData[MAX_PATH] = { 0 };
gets_s(sendData);
printf("发送数据:%s\n", sendData);
sendto(sclient, sendData, strlen(sendData), 0, (sockaddr*)&sin, len);
char recvData[MAX_PATH];
int ret = recvfrom(sclient, recvData, MAX_PATH, 0, (sockaddr*)&sin, &len);
if (ret > 0)
{
recvData[ret] = 0x00;
printf("接收到信息:%s\n",recvData);
}
}
return 0;
}
测试

参考

《Windows黑客编程技术详解》