套接字I/O的阻塞模型是最常见的网络模型,也是在网络编程中最早接触的一个模型。因为它是阻塞的,所以我们一般都会结合线程一起使用,如将acceptrecv等操作放到单独的线程,防止程序的主线程被阻塞住。

下面的示例为了演示阻塞模型使用的基本流程没有将其放入到独立的线程中。

一、服务端

服务端大致流程:

  1. 创建Socket
  2. Bind端口
  3. 开始Listen
  4. accept客户端连接(一般在子线程中不间断accept)
  5. send数据到客户端(也可以recv)
  6. close 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
#include <winsock2.h>
#include <iostream>
#include <assert.h>

using namespace std;

#pragma comment(lib, "Ws2_32.lib")

const u_short kPort = 10001;
const std::string kHelloServer = "hello, I'm server.";

int main()
{
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(2, 2);
WSAStartup(wVersionRequested, &wsaData);

SOCKET socket_ = INVALID_SOCKET;
SOCKET s_ = INVALID_SOCKET;

do
{
// (1)
socket_ = ::socket(AF_INET, SOCK_STREAM, 0);
if (socket_ == INVALID_SOCKET) {
std::cout << "create socket failed, GLE: " << WSAGetLastError() << std::endl;
break;
}


// (2)
struct sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(kPort);
if (bind(socket_, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR) {
std::cout << "bind failed, GLE: " << WSAGetLastError() << std::endl;
break;
}


// (3)
if (listen(socket_, 5) == SOCKET_ERROR) {
std::cout << "listen failed, GLE: " << WSAGetLastError() << std::endl;
break;
}
std::cout << "listen on port: " << kPort << std::endl;


// (4)
while (true)
{
struct sockaddr_in addr_c = { 0 };
int addr_len = sizeof(addr_c);
SOCKET s = accept(socket_, reinterpret_cast<sockaddr*>(&addr_c), &addr_len);
if (s == SOCKET_ERROR) {
std::cout << "accept failed, GLE: " << WSAGetLastError() << std::endl;
break;
}
std::cout << "new connection" << endl;

// (5)
// 此处使用while循环send,详见 三、流协议
int left = kHelloServer.length();
int idx = 0;
while (left > 0) {
int err = send(s, (const char*)(kHelloServer.c_str() + idx), left, 0);
if (err == SOCKET_ERROR) {
std::cout << "send failed, GLE: " << WSAGetLastError() << std::endl;
break;
}

left -= err;
idx += err;

std::cout << "bytes sent: " << err << std::endl;
}
}
} while (false);


// (6)
closesocket(socket_);
closesocket(s_);

WSACleanup();
return 0;
}

二、客户端

客户端大致流程:

  1. 创建Socket
  2. connect服务端
  3. recv接收数据从服务端(也可以send)
  4. close 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
#include <iostream>
#include <winsock2.h>
#include <assert.h>
#pragma comment(lib, "Ws2_32.lib")

const std::string kIP = "127.0.0.1";
const u_short kPort = 10001;
const std::string kHelloClient = "hello, I'm client.";

int main()
{
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(2, 2);
WSAStartup(wVersionRequested, &wsaData);

SOCKET socket_ = INVALID_SOCKET;

do
{
// (1)
socket_ = ::socket(AF_INET, SOCK_STREAM, 0);
if (socket_ == INVALID_SOCKET) {
std::cout << "create socket failed, GLE: " << WSAGetLastError() << std::endl;
break;
}

// (2)
struct sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(kIP.c_str());
addr.sin_port = htons(kPort);
if (connect(socket_, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR) {
std::cout << "connect failed, GLE: " << WSAGetLastError() << std::endl;
break;
}

// (3)
char buf[100] = { 0 };
int err = recv(socket_, buf, 100, 0);
if (err > 0) {
std::cout << "recv: " << buf << std::endl;
}
else if (err == 0) {
std::cout << "connection closed." << std::endl;
break;
}
else {
std::cout << "recv failed, GLE: " << WSAGetLastError() << std::endl;
break;
}
} while (false);


// (4)
closesocket(socket_);

WSACleanup();
return 0;
}

三、流协议

由于大多数面向连接的协议(如TCP)也是流协议。在流协议中,发送者和接收者可以将数据分解成小块数据,或将数据合并成大块数据。对于流套接字上收发数据所有用的函数(如send, recv),需要知道的是:它们不能保证要求进行读取或写入的数据量。比如用send发送一个有1024字节的字符缓冲区时,send函数可能返回的已发出的字节数少于1024。因为对每个收发数据的套接字来说,系统都为它们分配了充足的缓冲区空间,所以send的返回值将被设为已经发送的字节数。

针对这种情况,要保证缓冲区所有数据都被发送出去,可以采用下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int left = kHelloServer.length();
int idx = 0;
while (left > 0) {
int err = send(s, (const char*)(kHelloServer.c_str() + idx), left, 0);
if (err == SOCKET_ERROR) {
std::cout << "send failed, GLE: " << WSAGetLastError() << std::endl;
break;
}

left -= err;
idx += err;

std::cout << "bytes sent: " << err << std::endl;
}

对于接收数据来说,也可以采用上面的方式,但意义不大,因为我们一般都是循环的、不间断的接收数据,很少有上面的例子中的,只接收一次的情况。