为什么TCP服务端需要调用bind函数而客户端通常不需要呢?

972 篇文章 329 订阅
147 篇文章 46 订阅

       那一年, 某哥让我写个tcp服务端客户端程序, 我草草写完, 然后他检查,并问我, 为什么客户端不用bind呢? 然后, 我卡壳了, 好尴尬啊。 现在, 我们来一起彻底了解一下这个问题。

       先看看bind函数是干啥的。bind函数就是绑定, 将一个socket绑定到一个地址上, 也可以这么说:bind函数对一个socket进行命名(注意:socket名称包括三要素: 协议, ip,  port)。

 

       我们来看看最常见的方式, tcp服务端用bind, 但是tcp的客户端不用bind. 

       服务端程序为:

 

#include <stdio.h>
#include <winsock2.h> // winsock接口
#pragma comment(lib, "ws2_32.lib") // winsock实现

int main()
{
	WORD wVersionRequested;  // 双字节,winsock库的版本
	WSADATA wsaData;         // winsock库版本的相关信息
	
	wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
	

	// 加载winsock库并确定winsock版本,系统会把数据填入wsaData中
	WSAStartup( wVersionRequested, &wsaData );
	

	// AF_INET 表示采用TCP/IP协议族
	// SOCK_STREAM 表示采用TCP协议
	// 0是通常的默认情况
	unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;

	addrSrv.sin_family = AF_INET; // TCP/IP协议族
	addrSrv.sin_addr.S_un.S_addr = inet_addr("0.0.0.0"); // socket对应的IP地址
	addrSrv.sin_port = htons(8888); // socket对应的端口

	// 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程)
	bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

	// 将socket设置为监听模式,5表示等待连接队列的最大长度
	listen(sockSrv, 5);

	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);

	while(1)
	{
		// sockSrv为监听状态下的socket
		// &addrClient是缓冲区地址,保存了客户端的IP和端口等信息
		// len是包含地址信息的长度
		// 如果客户端没有启动,那么程序一直停留在该函数处
		unsigned int sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
		
		char sendBuf[100] = {0};
		sprintf(sendBuf,"%s", inet_ntoa(addrClient.sin_addr)); // 将客户端的IP地址保存下来
		send(sockConn, sendBuf, strlen(sendBuf) + 1, 0); // 发送数据到客户端,最后一个参数一般设置为0

		char recvBuf[100] = {0};
		recv(sockConn, recvBuf, 100 - 1, 0); // 接收客户端数据,最后一个参数一般设置为0
		printf("%s\n", recvBuf);

		getchar();

		closesocket(sockConn);
	}
		
	closesocket(sockSrv);
	WSACleanup();
	
	return 0;
}

      启动服务端。

 

 

      再看客户端程序:

 

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

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

	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8888);
	connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

	char recvBuf[100] = {0};
	recv(sockClient, recvBuf, 100, 0);
	printf("%s\n", recvBuf);
	send(sockClient, "hello world", strlen("hello world") + 1, 0);

	getchar();

	closesocket(sockClient);
	WSACleanup();

	return 0;
}

      启动它。

 

 

     然后, 我们在cmd中查看连接情况, 结果如下:

C:\Documents and Settings\Administrator>netstat -nao | findstr 8888
  TCP    0.0.0.0:8888           0.0.0.0:0              LISTENING       11256
  TCP    127.0.0.1:2964         127.0.0.1:8888         ESTABLISHED     13688
  TCP    127.0.0.1:8888         127.0.0.1:2964         ESTABLISHED     11256

       

     可以看到, 客户端的端口号是2964. 实际上, 这个端口号是操作系统随机分配的, 在分配的时候, 操作系统会保证不与现有的端口冲突。  好, 关掉这两个进程。 我们再重启服务端, 然后再重启客户端, 建立新的tcp连接, 我们再在cmd中查一次, 结果为:

C:\Documents and Settings\Administrator>netstat -nao | findstr 8888
  TCP    0.0.0.0:8888           0.0.0.0:0              LISTENING       11256
  TCP    127.0.0.1:2964         127.0.0.1:8888         ESTABLISHED     13688
  TCP    127.0.0.1:8888         127.0.0.1:2964         ESTABLISHED     11256


C:\Documents and Settings\Administrator>netstat -nao | findstr 8888
  TCP    0.0.0.0:8888           0.0.0.0:0              LISTENING       13296
  TCP    127.0.0.1:3156         127.0.0.1:8888         ESTABLISHED     13628
  TCP    127.0.0.1:8888         127.0.0.1:3156         ESTABLISHED     13296

 

      我们发现, 客户端的端口编程了3156, 和上次的 2964不一致, 这就印证了操作系统会随机分配客户端端口这个说法。

 

      以上就是最经典的服务端bind(这个是必须的), 客户端不bind.  那么, 我们自然要问, 客户端可不可以bind呢? 我们来实践一下:

      服务端测程序还是不变,依然为:

 

#include <stdio.h>
#include <winsock2.h> // winsock接口
#pragma comment(lib, "ws2_32.lib") // winsock实现

int main()
{
	WORD wVersionRequested;  // 双字节,winsock库的版本
	WSADATA wsaData;         // winsock库版本的相关信息
	
	wVersionRequested = MAKEWORD(1, 1); // 0x0101 即:257
	

	// 加载winsock库并确定winsock版本,系统会把数据填入wsaData中
	WSAStartup( wVersionRequested, &wsaData );
	

	// AF_INET 表示采用TCP/IP协议族
	// SOCK_STREAM 表示采用TCP协议
	// 0是通常的默认情况
	unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN addrSrv;

	addrSrv.sin_family = AF_INET; // TCP/IP协议族
	addrSrv.sin_addr.S_un.S_addr = inet_addr("0.0.0.0"); // socket对应的IP地址
	addrSrv.sin_port = htons(8888); // socket对应的端口

	// 将socket绑定到某个IP和端口(IP标识主机,端口标识通信进程)
	bind(sockSrv,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

	// 将socket设置为监听模式,5表示等待连接队列的最大长度
	listen(sockSrv, 5);

	SOCKADDR_IN addrClient;
	int len = sizeof(SOCKADDR);

	while(1)
	{
		// sockSrv为监听状态下的socket
		// &addrClient是缓冲区地址,保存了客户端的IP和端口等信息
		// len是包含地址信息的长度
		// 如果客户端没有启动,那么程序一直停留在该函数处
		unsigned int sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
		
		char sendBuf[100] = {0};
		sprintf(sendBuf,"%s", inet_ntoa(addrClient.sin_addr)); // 将客户端的IP地址保存下来
		send(sockConn, sendBuf, strlen(sendBuf) + 1, 0); // 发送数据到客户端,最后一个参数一般设置为0

		char recvBuf[100] = {0};
		recv(sockConn, recvBuf, 100 - 1, 0); // 接收客户端数据,最后一个参数一般设置为0
		printf("%s\n", recvBuf);

		getchar();

		closesocket(sockConn);
	}
		
	closesocket(sockSrv);
	WSACleanup();
	
	return 0;
}

      启动它。

 

 

      客户端采用bind, 如下:

 

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

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

	SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);


	// tcp客户端也要玩玩bind啦, 童鞋们!!!
	SOCKADDR_IN addrClient;
	addrClient.sin_family = AF_INET; 
	addrClient.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 客户端地址
	addrClient.sin_port = htons(7777); // 客户端为7777端口
	bind(sockClient,(SOCKADDR*)&addrClient, sizeof(SOCKADDR)); // 客户端也来玩玩绑定


	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 服务器地址
	addrSrv.sin_family = AF_INET;
	addrSrv.sin_port = htons(8888);
	connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

	char recvBuf[100] = {0};
	recv(sockClient, recvBuf, 100, 0);
	printf("%s\n", recvBuf);
	send(sockClient, "hello world", strlen("hello world") + 1, 0);

	getchar();

	closesocket(sockClient);
	WSACleanup();

	return 0;
}

      好, 启动它。

 

 

      这样, tcp的还是建立了正常的连接, 我们在cmd中查看一下:



C:\Documents and Settings\Administrator>netstat -nao | findstr 8888
  TCP    0.0.0.0:8888           0.0.0.0:0              LISTENING       11256
  TCP    127.0.0.1:2964         127.0.0.1:8888         ESTABLISHED     13688
  TCP    127.0.0.1:8888         127.0.0.1:2964         ESTABLISHED     11256


C:\Documents and Settings\Administrator>netstat -nao | findstr 8888
  TCP    0.0.0.0:8888           0.0.0.0:0              LISTENING       13296
  TCP    127.0.0.1:3156         127.0.0.1:8888         ESTABLISHED     13628
  TCP    127.0.0.1:8888         127.0.0.1:3156         ESTABLISHED     13296


C:\Documents and Settings\Administrator>netstat -nao | findstr 8888
  TCP    0.0.0.0:8888           0.0.0.0:0              LISTENING       12916
  TCP    127.0.0.1:7777         127.0.0.1:8888         ESTABLISHED     13176
  TCP    127.0.0.1:8888         127.0.0.1:7777         ESTABLISHED     12916

        可以看到, 客户端的端口号为7777, 反复多次做这个实验, 发下端口号都是7777. 由此可见, 客户端自己指定端口是成功的, 客户端也是可以bind的。

 

        现在, 我们可算是看清楚了, tcp服务端必须有bind, 客户端通常不用bind,  当然如果你够无聊, 那也可以用一下bind. 在这里, 我要说一下了: 客户端用bind的程序很容易出问题, 你想想啊, 操作系统指定的不会冲突的随机端口难道不比你自己指定的容易冲突的固定端口好?

        在很多场景下, 我们要在一个pc上开启多个客户端进程, 如果指定固定端口, 必然会造成端口冲突, 影响通信!所以, 我们就不要费力不讨好了, 客户端就不要指定端口了, 让操作系统来搞。这样,实际上就是操作系统对客户端的socket进行了隐式的命名(名称中的端口是随机的)。

 

        ok, 该休息了, 明天继续聊其他话题。


 

 

  • 34
    点赞
  • 99
    收藏
    觉得还不错? 一键收藏
  • 28
    评论
好的,可以的。不过在整合Netty和Spring Boot之前,我们需要先了解一下RPC的概念和原理。 RPC,全称为Remote Procedure Call,即远程过程调用。它的作用是让分布式系统中的不同节点之间能够像本地调用一样,通过网络调用远程节点的方法或函数,从而实现节点之间的数据交互和通信。 在RPC中,通常会有一个服务提供者和一个服务消费者。服务提供者会将自己的方法或函数暴露出来,供服务消费者调用。服务消费者则会通过网络向服务提供者发送请求,获取响应结果。 在进行RPC调用时,服务提供者和服务消费者之间需要进行相互校验,以确保双方都是可信的。常用的校验方式包括基于Token的认证和基于SSL的加密通信。 接下来,我们可以通过Netty来实现客户端服务端之间的通信。具体实现方式如下: 1. 在Spring Boot项目中引入Netty的依赖,例如: ```xml <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.36.Final</version> </dependency> ``` 2. 实现服务提供者和服务消费者的代码逻辑,在其中包含RPC校验的逻辑。 3. 在服务提供者中,创建Netty的ServerBootstrap对象,并设置相关参数,例如: ```java ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(new NioEventLoopGroup()) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new RpcDecoder(RpcRequest.class), new RpcEncoder(RpcResponse.class), new RpcHandler()); } }); ChannelFuture future = bootstrap.bind().sync(); future.channel().closeFuture().sync(); ``` 其中,RpcDecoder和RpcEncoder用于将RPC请求和响应对象转换为字节数组,RpcHandler用于处理RPC请求,并返回响应结果。 4. 在服务消费者中,创建Netty的Bootstrap对象,并设置相关参数,例如: ```java Bootstrap bootstrap = new Bootstrap(); bootstrap.group(new NioEventLoopGroup()) .channel(NioSocketChannel.class) .remoteAddress(new InetSocketAddress(host, port)) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new RpcEncoder(RpcRequest.class), new RpcDecoder(RpcResponse.class), new RpcProxyHandler()); } }); RpcProxyHandler rpcProxyHandler = bootstrap.connect().sync().channel().pipeline().get(RpcProxyHandler.class); ``` 其中,RpcProxyHandler用于发送RPC请求,并返回响应结果。 5. 最后,在服务消费者中调用服务提供者的方法即可,例如: ```java HelloService helloService = rpcProxyHandler.create(HelloService.class); String result = helloService.sayHello("world"); ``` 这样,我们就可以通过Spring Boot和Netty实现RPC调用和网络通信了。需要注意的是,在实际应用中,我们还需要考虑并发访问、性能优化、服务治理等方面的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值