Socket编程篇三

Posted mick_seu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket编程篇三相关的知识,希望对你有一定的参考价值。

下面我们实现下回声客户端。

所谓“回声”,是指客户端向服务器发送一条数据,服务器再将数据原样返回给客户端

代码相对于 篇一 与 篇二 并没有太多变化。如下所示:

服务器端:

#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int BUFFER_SIZE = 4096;
const int SERVER_PORT = 2222;

int main()
{
	int server_socket;
	char buff[BUFFER_SIZE];
	int n;

	server_socket = socket(AF_INET, SOCK_STREAM, 0);
	assert(server_socket != -1);

	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);
	assert(listen(server_socket, 5) != -1);
	
	struct sockaddr_in client_addr;
	socklen_t client_addr_len = sizeof(client_addr);

	while(1)
	{
		printf("waiting...\\n");
		int connfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
		if(connfd == -1)
			continue;
		printf("connect from %s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
		n = recv(connfd, buff, BUFFER_SIZE, 0);
		send(connfd, buff, n, 0);
		close(connfd);
	}
	close(server_socket);

	return 0;
}



客户端:

#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int BUFFER_SIZE = 4096;
const int SERVER_PORT = 2222;

int main()
{
	int client_socket;
	const char *server_ip = "127.0.0.1";
	char buffSend[BUFFER_SIZE];
	char buffRecv[BUFFER_SIZE];
	int n;
	fgets(buffSend, BUFFER_SIZE, stdin);

	client_socket = socket(AF_INET, SOCK_STREAM, 0);
	assert(client_socket != -1);

	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = inet_addr(server_ip);

	assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);
	assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1);
	n = recv(client_socket, buffRecv, BUFFER_SIZE, 0);
	buffRecv[n] = '\\0';
	printf("echo: %s\\n", buffRecv);

	close(client_socket);

	return 0;
}




Socket缓冲区


每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

Socket的写函数并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由 TCP / UDP 协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是 TCP / UDP 协议负责的事情。

TCP / UDP 协议独立于 Socket读写 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。



Socket 的I/O缓冲区示意图


I/O缓冲区特性如下:
1)I/O缓冲区在每个 Socket 中单独存在;
2)I/O缓冲区在创建 Socket 时自动生成;
3)即使关闭 Socket 也会继续传送输出缓冲区中遗留的数据;
4)关闭 Socket 将丢失输入缓冲区中的数据。




阻塞模式


对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:
1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据;

2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒;

3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入;


4) 直到所有数据被写入缓冲区 write()/send() 才能返回。


当使用 read()/recv() 读取数据时:
1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来;


2) 如果能读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取;

3) 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞;

这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。


以上说明:

数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。

例如,write()/send() 重复执行三次,每次都发送字符串"abc",那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。


假设我们希望客户端每次发送一位学生的学号,让服务器端返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号。例如第一次发送 1,第二次发送 3,服务器可能当成 13 来处理,返回的信息显然是错误的。


这就是 TCP协议 数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收,也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。


下面的代码演示了粘包问题,客户端连续三次向服务器端发送数据,服务器端却一次性接收到所有数据:

服务器端:

#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int BUFFER_SIZE = 4096;
const int SERVER_PORT = 2222;

int main()
{
	int server_socket;
	char buff[BUFFER_SIZE];
	int n;

	server_socket = socket(AF_INET, SOCK_STREAM, 0);
	assert(server_socket != -1);

	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);
	assert(listen(server_socket, 5) != -1);
	
	struct sockaddr_in client_addr;
	socklen_t client_addr_len = sizeof(client_addr);

	while(1)
	{
		printf("waiting...\\n");
		int connfd = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
		if(connfd == -1)
			continue;
		printf("connect from %s:%d\\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
		sleep(5);                            //等待5s后再接收数据,其实就是保证客户端发送的数据都已到达服务器端输入缓冲区
		n = recv(connfd, buff, BUFFER_SIZE, 0);
		send(connfd, buff, n, 0);
		close(connfd);
	}
	close(server_socket);

	return 0;
}

客户端:

#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

const int BUFFER_SIZE = 4096;
const int SERVER_PORT = 2222;

int main()
{
	int client_socket;
	const char *server_ip = "127.0.0.1";
	char buffSend[BUFFER_SIZE];
	char buffRecv[BUFFER_SIZE];
	int n;
	fgets(buffSend, BUFFER_SIZE, stdin);

	client_socket = socket(AF_INET, SOCK_STREAM, 0);
	assert(client_socket != -1);

	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = inet_addr(server_ip);

	assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);
	for(int i = 0; i < 3; ++i)                                                      //重复发送3次
		assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1);
	n = recv(client_socket, buffRecv, BUFFER_SIZE, 0);
	buffRecv[n] = '\\0';
	printf("echo: %s\\n", buffRecv);

	close(client_socket);

	return 0;
}

其实就是对上述代码稍作修改即可。




优雅的断开连接--shutdown()


调用 close() 函数意味着完全断开连接,即不能发送数据也不能接收数据,这种“生硬”的方式有时候会显得不太“优雅”。


close() 断开连接


主机A发送完数据后,单方面调用 close() 断开连接,之后主机A、B都不能再接受对方传输的数据。实际上,是完全无法调用与数据收发有关的函数。

一般情况下这不会有问题,但有些特殊时刻,需要只断开一条数据传输通道,而保留另一条。

使用 shutdown() 函数可以达到这个目的,它的原型为:

int shutdown(int sock, int howto);
howto 在 Linux 下有以下取值:
1)SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
2)SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
3)SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。


close() 和 shutdown() 的区别:

确切地说,close() 用来关闭套接字,将套接字描述符从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。

shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() 将套接字从内存清除。调用 close() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。

默认情况下,close() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。




通过域名获取IP地址


客户端中直接使用IP地址会有很大的弊端,一旦IP地址变化(IP地址会经常变动),客户端软件就会出现错误。而使用域名会方便很多,注册后的域名只要每年续费就永远属于自己的,更换IP地址时修改域名解析即可,不会影响软件的正常使用。

域名仅仅是IP地址的一个助记符,目的是方便记忆,通过域名并不能找到目标计算机,通信之前必须要将域名转换成IP地址。

gethostbyname() 函数可以完成这种转换,它的原型为:

struct hostent *gethostbyname(const char *hostname);

hostname 为主机名,也就是域名。使用该函数时,只要传递域名字符串,就会返回域名对应的IP地址。返回的地址信息会装入 hostent 结构体,该结构体的定义如下:

struct hostent{
    char *h_name;  //official name
    char **h_aliases;  //alias list
    int  h_addrtype;  //host address type
    int  h_length;  //address lenght
    char **h_addr_list;  //address list
}

从该结构体可以看出,不只返回IP地址,还会附带其他信息,我们只需关注最后一个成员 h_addr_list。下面是对各成员的说明:
1)h_name:官方域名(Official domain name)。官方域名代表某一主页,但实际上一些著名公司的域名并未用官方域名注册。
2)h_aliases:别名,可以通过多个域名访问同一主机。同一IP地址可以绑定多个域名,因此除了当前域名还可以指定其他域名。
3)h_addrtype:gethostbyname() 不仅支持 IPv4,还支持 IPv6,可以通过此成员获取IP地址的地址族(地址类型)信息,IPv4 对应 AF_INET,IPv6 对应 AF_INET6。
4)h_length:保存IP地址长度。IPv4 的长度为4个字节,IPv6 的长度为16个字节。
5)h_addr_list:这是最重要的成员。通过该成员以整数形式保存域名对应的IP地址。对于用户较多的服务器,可能会分配多个IP地址给同一域名,利用多个服务器进行均衡负载。


hostent 结构体变量的组成如下图所示:



下面简单运用一下该函数:

#include <iostream>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

int main()
{
	char hostname[100];
	cin >> hostname;
	struct hostent *host = gethostbyname(hostname);
	char **pc;
	if(host)
	{
		//official name
		if(host -> h_name)
			cout << "official name:" << '\\t' << host -> h_name << endl;
		//alias list
		cout << "alias list:" << endl;
		pc = host -> h_aliases;
		while(*pc != NULL)
		{
			 cout << '\\t' << *pc++ << endl;
		}
		//host address type and address length
		if(host -> h_addrtype == AF_INET)
			cout << "host address type: ipv4\\t"<< host -> h_length << "-byte" << endl;
		else if(host -> h_addrtype == AF_INET6)
			cout << "host address type: ipv6\\t" << host -> h_length << "-byte" << endl;
		//address list
		pc = host -> h_addr_list;
		cout << "address list:" << endl;
		while(*pc != NULL)
		{
			 cout << '\\t' << inet_ntoa(*(struct in_addr*)pc++) << endl;
		}
	}
	else
		cout << "ERROR" << endl;
}


以上是关于Socket编程篇三的主要内容,如果未能解决你的问题,请参考以下文章

TCP之socket编程

linux网络编程中阻塞和非阻塞socket的区别

java socket udp 怎么删除缓冲区数据

socket编程

linux高性能网络编程读书笔记之socket

网络编程基础-socket