c++网络编程,服务器初步搭建

服务器

Posted by Jow on October 31, 2019

目录

  1. socket函数
  2. connect函数
  3. bind函数
  4. listen函数
  5. accept函数
  6. 客户端和服务器的连接过程
  7. send,recv函数
  8. 聊一聊端口

认真学习,增强自己的能力和知识面。

socket函数

在网络编程中,socket函数十分常见,函数的返回值对应了一个类似于文件句柄的数值,通过该数值可以进行后续的一系列 connect,bind函数的调用。

实质上就是在系统中为了收发数据开辟的一块缓冲区,这块缓冲区在系统的内核中是以数组的方式实现的,每个数组中存放着一个指向缓冲区的指针。而返回的数值是指针作为元素在数组中的位序。

1
int socket(int domin, int type, int protocol);

返回值:文件描述符

domin:用于指定创建嵌套字的时候所使用的协议簇。可选择值:

  • AF_UNIX: 本机内进行通信的套接字
  • AF_INET: 用于 IPv4 TCP/IP 协议
  • AF_INET: 用于 IPv6 TCP/IP 协议

type:用于指定嵌套字类型。可选值:

  • SOCK_STREAM:创建TCP流套接字
  • SOCK_DGRAM:创建UDP数据报套接字
  • SOCK_RAM:常见原始套接字

protocol:通常设定为数值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
#include<WinSock2.h>
#include<iostream>


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

using namespace std;

//创建socket

int createSocket();

int main()
{
	//初始化WSA  
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;
	if (WSAStartup(sockVersion, &wsaData) != 0)
	{
		cout << "win socket 初始化失败!" << endl;
		return 0;
	}

	for (int i = 0; i < 10; i++)
	{
		int sk = createSocket();
		cout << "嵌套字:" << sk << endl;
		closesocket(sk);
	}
	return 0;

}

int createSocket()
{

	//创建嵌套字
	SOCKET sk = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sk == INVALID_SOCKET)
	{
		cout << "create socket error !" << endl;
		return 0;
	}
	return sk;
}

如果不关闭socket的时候将会返回不同的socket。

connect函数

connect函数是用来与指定地址的一段进行通讯连接的,这个连接可以分为TCP的稳定连接,它是面向字节流的,有着稳妥的确认-收发等一系列安全措施。如果对数据正确性要求高使用它即可。

UDP这样的面向数据报的连接方式,它其实是无连接的,网络传输实时性好,如果对响应的时间要求较高的话,建议使用UDP。

connect函数多半用于客户端向服务器发送请求,通过connect的参数来指出发送请求的目的服务器。

在这里要知道一下几个常用函数的数据结构。

struct sockaddr_in:在网络编程中常用来存放通信端的IP地址端口号等信息的结构体。

1
2
3
4
5
6
7
struct sockaddr_in
{
	unsigned short sin_family;//地址类型AF_INET(首选,常用),AF_INET6
	unsigned short sin_pot;//端口号,配合htons将主机小端模式转化为大端模式  //htons:host to net (short)
	struct in_addr;//ip地址
	unsigned chat sin_zero[8];//填充字节。
}

在这里,操作系统常为小端模式;通讯协议为大端模式。

小端模式是高位存在高位,低位存低位。

大端模式高位存低位,低位存高位。

这里我们再聊一下sockaddr,在使用方面通常socketaddr_in来进行复制操作,然后通过强制转化的方式将其转换为sockaddr来作为函数的参数来使用。

connect函数:

1
2
int connect(int sockfd, const struct sockaddr * serv_addr, socket_len addrlen);

  1. scokfd:通过调用socket函数正确执行后的返回值。
  2. serv_addr:运行connect函数的客户端发送请求的服务端网络地址变量
  3. addrlen:第二个参数服务器网络地址变量的长度。

在使用参数2的时候,一般创建struct sockaddr_in serv_addr变量,根据实际的应用为其赋值,然后对其取地址强制转换成struct sockaddr指针结构。(strcut sockaddr *)(&serv_addr)。

inet_aton(const char * string, struct in_addr *addr):这个方法的作用,将ascii码表示的IP地址转换为一个32位的网络地址(使用struct inn_addr表示)。如果转换正确返回值为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
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
#include<WinSock2.h>
#include<iostream>

#define PORT 9003


#pragma comment(lib,"ws2_32.lib")  
#pragma warning(disable:4996)

using namespace std;

//创建socket

int createSocket();
int connectServer(SOCKET sk);

void getIP(char* szHostaddress);

int main()
{
	//初始化WSA  
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA wsaData;
	if (WSAStartup(sockVersion, &wsaData) != 0)
	{
		cout << "win socket 初始化失败!" << endl;
		return 0;
	}

	SOCKET sk = createSocket();
	connectServer(sk);
	return 0;

}

void getIP(char* szHostAddress)
{
	char szHostname[100] = "localhost";
	//得到来自服务器的名字
	if (gethostname(szHostname, sizeof(szHostname) != SOCKET_ERROR))
	{
		//解析主机名获取主机地址
		HOSTENT* pHostEnt = gethostbyname(szHostname);
		if (pHostEnt != NULL)
		{
			sprintf(szHostAddress, "%d.%d.%d.%d",
				(pHostEnt->h_addr_list[0][0] & 0x00ff),
				(pHostEnt->h_addr_list[0][1] & 0x00ff),
				(pHostEnt->h_addr_list[0][2] & 0x00ff),
				(pHostEnt->h_addr_list[0][3] & 0x00ff)
				);
			cout << szHostAddress << endl;
			char adress[200];
			//IP地址,这种方法或许更好
			for (int i = 0; pHostEnt->h_addr_list[i]; i++) {
				strcpy(adress, inet_ntoa(*(struct in_addr*)pHostEnt->h_addr_list[i]));
				//adress = inet_ntoa(*(struct in_addr*)pHostEnt->h_addr_list[i]);
				cout << adress << endl;
				printf("IP addr %d: %s\n", i + 1, inet_ntoa(*(struct in_addr*)pHostEnt->h_addr_list[i]));
			}
		}
	}
}

int createSocket()
{
	//创建嵌套字
	SOCKET sk = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sk == INVALID_SOCKET)
	{
		cout << "create socket error !" << endl;
		closesocket(sk);
		WSACleanup();
		return FALSE;
	}
	return sk;
}

int connectServer(SOCKET sockfd)
{
	//赋值通信地址
	char HostAddress[200];
	//获取IP地址信息
	getIP(HostAddress);
	SOCKADDR_IN serv_addr;

	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(PORT);
	serv_addr.sin_addr.s_addr = inet_addr(HostAddress);

	if (SOCKET_ERROR == connect(sockfd, (const sockaddr*)& serv_addr, sizeof(serv_addr)))
	{
		cout << "failed to connect server!" << endl;
		closesocket(sockfd);
		WSACleanup();
		return FALSE;
	}
	return 0;
}

在这里我们使用几个函数,现在我们来分别的介绍一下这些函数的作用,首先gethostbyname,这个函数的作用是让我们可以根据hostname来获取ip地址,我们可以通过它来获取本地的ip地址,或者其他网址的IP地址。

下面我们通过一个例子获取百度的ip地址:

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
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main(){
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);
    struct hostent *host = gethostbyname("www.baidu.com");
    if(!host){
        puts("Get IP address error!");
        system("pause");
        exit(0);
    }
    //别名
    for(int i=0; host->h_aliases[i]; i++){
        printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
    }
    //地址类型
    printf("Address type: %s\n", (host->h_addrtype==AF_INET) ? "AF_INET": "AF_INET6");
    //IP地址
    for(int i=0; host->h_addr_list[i]; i++){
        printf("IP addr %d: %s\n", i+1, inet_ntoa( *(struct in_addr*)host->h_addr_list[i] ) );
    }
    system("pause");
    return 0;
}
/**
运行结果:
Aliases 1: www.baidu.com
Address type: AF_INET
IP addr 1: 61.135.169.121
IP addr 2: 61.135.169.125
**/

在这里我们看到了inet_ntoa函数,它的作用是将网络地址转换成“.”点隔的字符串格。

当我们需要将“.”点隔的字符串转换成无符号长整型可以使用inet_addr(“132.241.5.10”)。

bind函数

在网络通信程序中,通过ip:port来确定网络中的唯一一台主机上面的运行的进程,对于客户端而言,通过向指定主机上面端口来发送信息请求对应的服务。服务器在网络编程中指代运行服务器程序的进程,这个是从狭义上来说的,从广义上来说也可以是运行服务器进行的主机。

前者是通过端口号来指定,而后者是通过IP地址来确定。

那么,socket套接字与运行服务器进程的端口号之间有什么联系呢?

这个问题的答案便是bind存在的意义。

服务器程序运行主要是为了接受来自各个端点的客户端的请求,然后根据这个请求给予不同的回复,对于服务器来说,接收信息也好,回复结果也罢,都需要指定的缓冲区来存放这些信息和数据。这个缓冲区则是socket函数的返回值所指定的系统分配的空间的数组下标。

而:IP:PORT则是标定程序运行的主机与进程号码。

那么通过函数bind将运行服务器程序的主机:进程号与缓冲区进行绑定的含义便是将通过socket函数向系统申请的系统空间与运行在当前系统上面的程序进行绑定。也就是为进程分配用来存放网络通信所需要的缓冲空间。

将socket返回的数值理解为空间可以这样来向:socket返回的数值与系统中的文件描述符fd是等价的,fd指向的是系统为文件在系统中开辟的空间,所以通过socket返回的socket_fd也对应着系统为其开辟的一块缓冲区。只不过,文件描述符的fd可以通过某种方式写入到文件中,在用户确认写入之后,将会把缓冲区中的数据写入到系统的磁盘中作为永久保存。而socket对象的sock_fd仅仅是一块缓冲区,当服务器与客户端之间的通信结束之后,该缓冲区通过closesocket(socket_fd);方法调用之后将会被系统回收,没有写到磁盘的机会。

下面来看一下bind函数的描述:

1
2
3
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);

// 返回值:成功为0,否则非0负数

sockfd:该参数便是socket函数的返回值。

myaddr:套接字描述字,要绑定的目标主机上的进程描述变量。

addrlen: 参数2的长度

在C/S通信模型中

client端使用函数顺序通常是:socket->connect->send/recv

server端使用函数顺序通常是:socket->bind->listen->send/recv

1
2
3
4
5
6
7
8
9
10
11
12
13
int getBindDone(SOCKET sk)
{
	SOCKADDR_IN server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = PORT;
	server_addr.sin_addr.s_addr = INADDR_ANY;
	if (SOCKET_ERROR == bind(sk,(const sockaddr*) &server_addr, sizeof(server_addr)))
	{
		closesocket(sk);
		WSACleanup();
		return FALSE;
	}
}

listen函数

listen函数是用于网络编程中的服务器端用于监听/聆听来自一个或者多个客户端的连接请求的,由于在实际应用中服务器与客户端的关系通常是一对多的情况,所以服务器端通常都会在为一个客户端请求提供服务的同时,又存储多个来不及服务的客户端请求,而这些来不及获得服务器响应或是提供服务的客户端请求会被存放到服务器的缓冲队列中。服务器每次完成一个客户端请求的时候,会从缓冲队列中取出一个请求并为之提供相应的服务。

而listen函数来说,必须要知道系统为服务器程序分配的缓冲区在哪里(通过socket函数返回值标定)以及缓冲队列的长度是多少(根据缓冲队列的长度来在嵌套字描述符指定的内存空间中为其缓冲队列分配空间)。

从上面的设定便可得知:系统会根据自身配置来为嵌套字描述符分配一块默认大小的空间,缓冲队列的空间是从这块系统默认大小的空间中进行划分的;如果设定的缓冲队列长度过大,导致队列长度大于系统大小的长度,便会导致错误。

listen函数的功能仅仅用来监听请求,和通过缓冲队列来组织一下服务器来不及的响应的请求,而非接收请求,接收请求是accept函数来执行的。

listen和accept函数的区别,假设服务器中有一个add计算和的函数,客户端发过来请求,请求调用服务器代码来执行该段代码,来计算a=999,b=1的时候的结果,服务器通过listen函数监听到来自客户端的请求,便把这个请求记录,此时服务器端的代码还是代码;一旦服务器决定accept这个请求了,那么系统便会将该程序代码加载到内存中,为代码声明变量,还有一些内部函数进行初始化,为其分配内存空间,以及存储返回值的空间,就像是程序和程序源文件的区别一样。

listen函数

1
2
int listen(int sockfd, int backlog);

在程序中默认函数的调用顺序便是bind -> listen。

bind是将服务器进程端口号与套接字描述符进行绑定,listen是通过套接字描述符来找到与该套接字描述符绑定的服务器进程,监听各个客户端发往该服务器进程的连接请求的。

  1. sockfd:这个参数便是刚刚执行过,用来指定主机上的端口进行绑定的套接字描述符。
  2. backlog:这个参数是用来指定服务器端最多可以接受多少个连接请求,也就是缓冲区中的缓冲队列的最大长度,一旦超过最大长度,服务器端便会拒绝接收连接请求。
1
2
3
4
5
6
7
//启动监听
	if (SOCKET_ERROR == listen(tcpServer, SOMAXCONN))
	{
		//如果失败,关闭嵌套字
		closesocket(tcpServer);
		WSACleanup();
	}

accept函数

accept函数常用在服务器端接收从客户端发来的请求信息,服务器程序一旦决定接收来自客户端的请求(通常情况下,请求可以是客户端请求读取服务器端的一个文件,或是请求调用服务器上的一个函数,但是无论那一种都需要服务器进程在本地为其提供一定的缓存空间,如果是文件,就会从服务器端的硬盘中通过系统调用将文件中的内容读取到内存中(缓存);如果是远程调用,便会调用服务器的某个进程,进程运行时需要创建变量和从系统堆栈上面获取运行空间的)就需要向系统申请相关的缓冲空间,而在socket函数的讲解中我们也讨论过了,系统为进程划分缓冲区的方式与系统通过文件描述符来为文件划分空间的方式类似,所以,一旦服务器accept客户端的请求成功,便会返回一个新的套接字描述符,而这个套接字描述符所指向的服务器本地系统中的缓冲空间,是用于client端和server端通信交换数据的缓冲空间。它与服务器启动的时候通过socket函数返回的嵌套字描述符所对应的空间不同,后者是用来接收并缓存来自各个不同client端的连接信息的缓冲队列的数据缓冲空间。

accept函数

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  1. sockfd:是服务器端通过调用正确调用socket -> bind -> listen 函数之后的用于指向存放多个客户端缓冲队列缓冲区的套接字描述符
  2. addr: 是用来保存发起连接请求的主机的地址与端口的结构体变量,就是存放服务器接收请求的客户端的网络地址与端口的结构体变量
  3. addrlen: 用来传入第二个参数类型长度

在这里还有一个问题,那就是服务器通过accept函数接收来自客户端的连接请求的方式,一共有两种:

  1. 阻塞方式:这种方式是通过socket方法创建出来的套接字描述符默认的通信方式,阻塞方式便是,当没有任何客户端向服务器发送连接请求,同时服务器端的用于存储客户端连接请求缓冲队列为空的情况下,服务器的代码执行一直卡在accept语句上不往下执行,直到接到来自客户端的连接请求才往下执行。
  2. 非阻塞方式:这种方式通过fcnt1函数作用于socket函数返回的嵌套字描述符,并将该嵌套字描述符传入到accept函数的首个参数上设定的。非阻塞的方式是指,当服务器的请求连接缓冲队列为空,同时没有接到任何来自客户端的连接请求的时候,代码执行到accept函数调用的时候,会立即返回-1告知主程序这种情况而不是一直等待不往下执行。

这两种方式是通过调用fcnt1这个函数来将阻塞/非阻塞信息标识到用于指向缓冲队列的嵌套字描述符来实现accept是以阻塞还是非阻塞的方式接收来自各个客户端的连接请求的。

fcnt1函数的使用方式非常有趣,这种函数的使用方式在unix编程中很常见,就像是有三个参数,第一个参数用作记录用户信号,第二个参数是告知用户信号发出什么动作,第三个参数是用作告诉用户信号动作的不同类型。

就像是第一个参数是一张纸,这张纸将会作为指示工人的命令,而fcnt1函数是一个打印机,把白纸送入打印机,第二个参数用于告知打印机(画画),第三个参数用于告知打印机画画{画山水画,画油画,画素描}。

待到将纸从打印机中取出的时候,纸上写着 (画画,画素描) 那么工人们便会{画画,画素描}。

同样第一个参数是一张纸,这张纸将会作为指示工人的命令,而 fcntl 函数是一个打印机,把白纸送入打印机,第二个参数用于告知打印机{打字},第三个参数用于告知打字{打毛笔字,打钢笔字,打黑体,打宋体}。

待到将纸从打印机中取出的时候,纸上写着 (打字,打毛笔字),那么工人们便会{打字,打毛笔字}。

这几种方式都可以通过不同的宏定义来实现,第二个参数就像是指定了第三个参数的变化阈值一样 。

所以对于 fcntl 函数来说 传入的 套接字描述符 sock_fd 就相当于是白纸,cmd 参数就相当于是命令 ,arg 参数就相当于是cmd命令所指定的指令集中的一条指令.

1
int fcntl ( int sock_fd , int cmd , long arg ) ;

在这里,要将 accept 的执行方式设定为非阻塞的便可以这样,fcntl ( 传入套接字描述符, F_SETFL , O_NONBLOCK ),待到传入的套接字描述符以上述的方式调用 fcntl 的时候,在被传入到 accept 函数中之后,便会指示 accept 以 O_NONBLOCK 的方式来执行了。

1
2
3
4
//等待连接传入
SOCKADDR_IN addrClient;
int addrClientLen = sizeof(addrClient);
SOCKET tcpclient = accept(tcpServer, (sockaddr*) &addrClient, & addrClientLen);

客户端和服务器的连接过程

客户端和服务器的连接,因为服务器的接受方式分为阻塞和非阻塞两种方式,所以在这里我们实现两种服务器接受方式。

  1. 如果 server 端接收消息的方式是阻塞的话,那么首先启动 server端的程序代码,待到 server 端因为没有接到任何的来自于 client 的请求信息而执行 accept 方法时处于等待状态。此时再来启动 client 端的代码。
  2. 如果 server 端接收消息的方式是非阻塞的话,那么在 client 端中以 connect 方法为循环变量,直到 connect 发送的连接请求得到 server端的回复信息之后,才会是循环结束,否则一直执行循环,不断的向 server 端发送请求连接的消息。
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
int GameServer::acceptListen()
{
	int addrClientLen = sizeof(addrClient);
	std::cout << "TCPServer 启动成功,等待连接。。。" << std::endl;
	//循环等待消息和连接
	while (true)
	{
		std::cout << "等待连接" << std::endl;
		SOCKET tcpclient = accept(tcpServer, (sockaddr*)& addrClient, &addrClientLen);
		if (INVALID_SOCKET == tcpclient)
		{
			std::cout << WSAGetLastError() << std::endl;
			closesocket(tcpServer);
			closesocket(tcpclient);
			WSACleanup();
			return -1;
		}
		std::cout << "客户端的连接信息:(" << inet_ntoa(addrClient.sin_addr) << ":" << addrClient.sin_port << ")" << std::endl;
		while (true)
		{
			char recvBuf[BUFLEN];
			ZeroMemory(recvBuf, sizeof(recvBuf));
			if (SOCKET_ERROR == recv(tcpclient, recvBuf, sizeof(recvBuf), 0))
			{
				std::cout << "接收消息失败!" << std::endl;
				closesocket(tcpServer);
				closesocket(tcpclient);
				WSACleanup();
				return -1;
			}
			std::cout << "客户端的发送过来的消息:" << recvBuf << std::endl;
			if (strcmp(recvBuf, ENDFLAG))
			{
				send(tcpclient, ENDFLAG, sizeof(ENDFLAG), 0);
				std::cout << "客户端不再发送消息" << std::endl;
				break;
			}
			//发送反馈数据
			char sendBuf[BUFLEN] = "Server to client!";
			if (SOCKET_ERROR == send(tcpclient, sendBuf, sizeof(sendBuf), 0))
			{
				std::cout << "发送消息失败!" << std::endl;
				closesocket(tcpServer);
				closesocket(tcpclient);
				WSACleanup();
				return -1;
			}
		}
	}
	return TRUE;
}

int GameClient::setDataToServer()
{
	while (true)
	{
		//向服务器发送数据
		char sendBuffer[BUFLEN];
		std::cin >> sendBuffer;
		std::cout << sizeof(sendBuffer) << std::endl;
		if (SOCKET_ERROR == send(clientSocket, sendBuffer, sizeof(sendBuffer), 0))
		{
			std::cout << "send data failed!" << std::endl;
			closesocket(clientSocket);
			WSACleanup();
			return -1;
		}
		std::cout << "send data successful!" << std::endl;
		//接收来自服务器的信息
		char recvBuf[BUFLEN];
		ZeroMemory(recvBuf, sizeof(recvBuf));
		if (SOCKET_ERROR == recv(clientSocket, recvBuf, sizeof(recvBuf), 0))
		{
			std::cout << "recv data failed!" << std::endl;
			closesocket(clientSocket);
			WSACleanup();
			return -1;
		}
		std::cout << "recv data from server:" << recvBuf << std::endl;
		if (strcmp(recvBuf, ENDFLAG))
		{
			break;
		}
	}
	return TRUE;
}


send,recv函数

当 Server 端从请求缓冲队列中获取一个连接请求并将其 accept(接收)之后,server 的运行系统会为其分配一块用来执行请求操作的缓冲空间。而这块缓冲空间是通过server端调用 accept 函数的返回值—- 一个新的套接字描述符来指向的。

当Client 端向 Server 端发送请求信息之后,connect 方法成功执行返回 0 的时候,系统会为其接收来自 Server 端的返回信息也分配一块缓冲空间,而这块缓冲空间是通过Client 端一开始通过 socket 方法返回的套接字描述符来唯一指定的。

这个地方会存在一个误区,client和server的缓冲空间是共享的么?思考一下,应该不是的。

  1. 首先知道,套接字描述符是用来标定系统为当前的进程划分的一块缓冲空间的,类似于文件描述符,不过二者有些区别;
  2. 其次应该知道的是,这块缓冲空间并不是一开始就被系统划分给进程的;
  3. 对于 server 端而言,划分系统缓冲空间的时刻是: 当server 决定接收来自 client 的连接请求,即 accept 方法成功执行,返回一个 > 0 的整数(也就是新的套接字描述符),系统才会为其分配缓冲空间,自然, 这块缓冲空间是通过 accept 新的套接字描述符来指定的
  4. 对于 client 端而言,划分系统缓冲空间的时刻是: 当 client 端执行 connect 函数正确的时候,connect 函数正确执行,说明此 client 端的连接请求已经被 server 端接收,剩下的就需要系统为 client 划分缓冲空间,用来接收来自于 server 端的返回结果。 这个时候,系统才会为其分配缓冲空间,而该缓冲空间使用 client 一开始创建套接字的 socket 函数的返回值标定即可。

值得记下的一点便是,send 是将内存中的数据放入到缓冲区中,而 recv 是将缓冲区中的数据提取到内存中,而内存便是我们使用各种变量和数据类型的地方。

send 参数便是普通变量参数; 而 recv 参数是 值-结果 参数, 值-结果参数常用作函数的返回值,有的是双向的,有的是单向的。

双向指的是:该参数传入的值会参与到函数的计算中,同时会将函数的运行结果赋给该参数作为函数的返回值。

单向指的是:该参数传入的数值是什么并不重要,重要的是该参数在被传入函数中,函数执行之后,该参数中的数值。

send函数

1
ssize_t send ( int s , const void *msg , size_t len , int flags ) ;
  1. s值的就是套接字描述符了,如果是 client 端调用 send 方法的话,s 便是 connect 正确执行之后,首次调用 socket 函数返回的套接字描述符。如果是 server 端调用 send 方法的话, s 便是 accept 正确执行之后的返回的新的套接字描述符。
  2. msg 是要从内存 —-> 放入缓冲区中的数据,通常都是将字符串指针强制转为 void *得到的
  3. len 是 msg 的长度
  4. flags 控制选项,对应一系列系统定义的宏,用来指定以何种方式来发送缓冲区的数据。常用的取值为 0 ,在特殊一点就是带外数据 (flags= MSG_OOB) 其中 OOB = out of band,带外数据一般用于紧急命令的传送,希望通信的对等端优先接收和处理的信息。

其返回值意义:

  1. 如果返回值为负数,说明将内存中的数据移动到缓冲区中操作失败。
  2. 如果返回值为 >=0 的数值,说明移动成功,返回的数值便是将内存中的多少个字节移动到了缓冲区中。而且这种情况仅仅说明数据移动成功,并不能表明成功发送缓冲区中的数据到对等端 。

recv函数

1
ssize_t recv ( int s , void *buf , size_t len , int flags ) ;
  1. s值的就是套接字描述符了,如果是 client 端调用 send 方法的话,s 便是 connect 正确执行之后,首次调用 socket 函数返回的套接字描述符。如果是 server 端调用 send 方法的话, s 便是 accept 正确执行之后的返回的新的套接字描述符。
  2. buf : 指向内存用于存放来自缓冲空间的指针 ,也就是接收来自缓冲区的数据,并将该数据存放到有 buf 指针指向的空间中
  3. buf 指向的内存空间的大小
  4. 控制选项,对应一系列宏变量,这些宏变量用来限制或是指定接收数据的类型。flags = 0 接收所有数据;flags = MSG_OBB 仅仅接收带外数据;flags = MSG_PEEK : 只查看数据而不读出数据;flags = MSG_WAITWALL : 为了减少从缓冲区频繁少量的读取数据到内存操作,只在 buf 指向的空间中存满数据的核实后,才返回。

关于recv函数也是阻塞的,当客户端和服务器都没有断开连接的时候,recv函数是阻塞的,当监听recv的一方发现另一方断开了连接,就会返回-1,说明对方已经断开连接。

聊一聊端口

之前一直在纠结客户端是怎么知道服务器的端口的呢,为什么别人在请求连接的时候没有带端口号呢,现在想想应该只是没有把端口号暴露出来,客户端应该还是知道怎么取到正确的端口号的,就像你访问百度,默认端口不就是80么。所以如果客户端不知道端口是肯定找不到正确的服务器的。

那么什么是端口呢?如果把IP地址比作一间房子 ,端口就是出入这间房子的门。真正的房子只有几个门,但是一个IP地址的端口 可以有65536(即:256×256)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从0 到65535(256×256-1)。端口其实就是队,操作系统为各个进程分配了不同的队,数据包按照目的端口被推入相应的队中,等待被进程取用,在极特殊的情况下,这个队也是有可能溢出的,不过操作系统允许各进程指定和调整自己的队的大小。一个数据包包括了文件,ip,和端口号,ip是为了服务器可以找到你的主机,端口号是你接受数据包的门户, 而所谓的端口监听,是指主机网络进程接受到IP数据包后,察看其的目标端口是不是自己的端口号,如果是的话就接受该数据包进行处理。

在服务器和客户端通信过程中,socket通信双方自己决定自己要用什么端口,服务器端只能决定自己listen的是那个端口,并不能决定客户端的端口;客户端也只能决定自己的端口,当然因为客户端是主动连接的, 所以它知道服务器的端口号,端口号是用来区分相同主机上的不同socket的,它相当于socket在本机这一端的名字,一个端口只能bind一次,占用了之后同时就不能再给别人用了,这么宝贵的资源都要留给listen的一方,主动连接的一方一般去用那些操作系统随机分配的端口号。bind也可以指定端口号为0,这种情况下就是随机绑定一个没有使用过的端口号,可以用来在建立连接之前就确定本端的端口号。作为listen的一方,所有被动建立起来的连接的本端端口都是listen的端口。只有源IP、源端口、目的IP、目的端口还有协议号都完全相同才会认为是同一个socket,所以被动建立连接的socket源端口号不同,目的端口号是可以相同的。

再说下客户端用不用bind的区别

无连接的socket的客户端和服务端以及面向连接socket的服务端通过调用bind函数来配置本地信息。使用bind函数时,通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。Bind()函数在成功被调用时返回0;出现错误时返回”-1”并将errno置为相应的错误号。需要注意的是,在调用bind函数时一般不要将端口号置为小于1024的值,因为1到1024是保留端口号,你可以选择大于1024中的任何一个没有被占用的端口号。

有连接的socket客户端通过调用Connect函数在socket数据结构中保存本地和远端信息,无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候打开端口。(当然也有特殊情况,linux系统中rlogin命令应当调用bind函数绑定一个未用的保留端口号,还有当客户端需要用指定的网络设备接口和端口号进行通信等等)

  1. 需要在建连前就知道端口的话,需要 bind
  2. 需要通过指定的端口来通讯的话,需要 bind

一般情况下客户端是不用调用bind函数的,一切都交给内核搞定。但是服务器要调用bind函数,这样子客户端才能确认连接那个端口。

再说一个问题,为什么你用浏览器打开一个网站不需要输入端口呢?不是说好客户端访问服务器需要指定端口号的吗?其实,是你的浏览器默认帮助你的ip地址后面加上了:80,80端口是默认的http端口,所以就不需要你输入网址时加端口啦。