【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;
}
时间: 2024-10-28 10:05:11

【Socket编程】篇三的相关文章

Python 基础之socket编程(三)

python 基础之socket编程(三) 前面实现的基于socket通信只能实现什么呢?在tcp协议的通信中就是一个用户说一句,服务端给你回一句,你再给服务端说一句,服务端再给你回一句,就这样一直友好的玩耍下去了.等等,又有一个用户来了,他呢也想和和服务端进行一下交流,于是他就给服务端发送了一条消息,之后等呀等不知过了多久,任然没有等到服务端给他发挥的消息,只有什么时候他就可以和服务端愉快的玩耍了呢?这个就需要第一个用户退出和服务器的链接,此时第二个客户端才会和服务端建立起链接,此时此刻,他才

python基础之socket编程(TCP三次握手和四次挥手)

TCP协议中中的三次握手和四次挥手 建立TCP需要三次握手才能建立,而断开连接则需要四次握手.整个过程如下图所示: 先来看看如何建立连接的. 首先Client端发送连接请求报文,Server段接受连接后回复ACK报文,并为这次连接分配资源.Client端接收到ACK报文后也向Server段发生ACK报文,并分配资源,这样TCP连接就建立了. 那如何断开连接呢?简单的过程如下: [注意]中断连接端可以是Client端,也可以是Server端. 假设Client端发起中断连接请求,也就是发送FIN报

Socket编程(三)★

★Sockets 使用TCP协议实现网络通信的Socket相关类(重点) TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的.可靠的.基于字节流的传输层通信协议 服务器端的ServerSocket类        客户端的Socket类 ①.服务器端通信流程                                                           ②. 客户端通信流程    1. 创建ServerSocket对象,绑定该程

iOS开发——网络编程OC篇&amp;Socket编程

Socket编程 一.网络各个协议:TCP/IP.SOCKET.HTTP等 网络七层由下往上分别为物理层.数据链路层.网络层.传输层.会话层.表示层和应用层. 其中物理层.数据链路层和网络层通常被称作媒体层,是网络工程师所研究的对象: 传输层.会话层.表示层和应用层则被称作主机层,是用户所面向和关心的内容. http协议   对应于应用层 tcp协议    对应于传输层 ip协议     对应于网络层 三者本质上没有可比性.  何况HTTP协议是基于TCP连接的. TCP/IP是传输层协议,主要

Android应用开发提高篇(4)-----Socket编程(多线程、双向通信)(转载)

转自:http://www.cnblogs.com/lknlfy/archive/2012/03/04/2379628.html 一.概述 关于Socket编程的基本方法在基础篇里已经讲过,今天把它给完善了.加入了多线程,这样UI线程就不会被阻塞:实现了客户端和服务器的双向通信,只要客户端发起了连接并成功连接后那么两者就可以随意进行通信了. 二.实现 在之前的工程基础上进行修改就可以了. MyClient工程的main.xml文件不用修改,只需要修改MyClientActivity.java文件

IP地址的三种表示格式及在Socket编程中的应用

转自:http://blog.csdn.net/hguisu/article/details/7449955 使用TCP/IP协议进行网络应用开发的朋友首先要面对的就是对IP地址信息的处理.IP地址其实有三种不同的表示格式:  1)Ascii(网络点分字符串)-        2) 网络地址(32位无符号整形,网络字节序,大头)        3)主机地址 (主机字节序)   IP地址是IP网络中数据传输的依据,它标识了IP网络中的一个连接,一台主机可以有多个IP地址,IP分组中的IP地址在网络

【Socket编程】篇一

参考自:http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html(吴秦) 1.Socket 简介 本地的进程间通信(IPC)有多种方法: 1)消息传递(PIPE.FIFO.消息队列等) 2)同步(互斥量.条件变量.读写锁.记录锁.信号量等) 3)共享内存(匿名的和具名的) 4)远程过程调用(Solaris门和Sun RPC) 在本地可以通过进程 PID 来标识一个进程,但是在网络中这是行不通的.其实 TCP/IP 协议族已经帮我们

8.17_Linux之bash shell脚本编程入门篇(三)之循环以及函数function的使用

bash shell脚本编程入门篇(三)之循环 什么是循环执行? 将某代码段重复运行多次 重复运行多少次: 循环次数事先已知 循环次数事先未知 有进入条件和退出条件 相关命令:for, while, until,selet, for命令的使用 作用: 依次将列表中的元素赋值给"变量名"; 每次赋值后即执行一次循环体; 直到列表中的元素耗尽,循环结束 命令格式: for 变量名 in 列表; do 循环体(正常执行的执行命令) 语句1 语句2 语句3 ... done 列表生成方式: (

Java Socket编程基础篇

原文地址:Java Socket编程----通信是这样炼成的 Java最初是作为网络编程语言出现的,其对网络提供了高度的支持,使得客户端和服务器的沟通变成了现实,而在网络编程中,使用最多的就是Socket.像大家熟悉的QQ.MSN都使用了Socket相关的技术.下面就让我们一起揭开Socket的神秘面纱. Socket编程 网络基础知识点: 两台计算机间进行通讯需要以下三个条件 IP地址.协议.端口号: IP地址:定位应用所在机器的网络位置.(比如家庭住址:北京市朝阳区XX街道XX小区) 端口号

PHP Socket 吉林快三跨度玩法平台开发编程进阶指南

?Linux吉林快三跨度玩法平台开发[大神源码论坛]dsluntan.com [布丁源码论坛]budingbbs.com 企娥3393756370 或者 Mac 环境:?安装有 Sockets 扩展:?了解 TCP/IP 协议. socket函数只是PHP扩展的一部分,编译PHP时必须在配置中添加 --enable-sockets 配置项来启用. 如果自带的PHP没有编译scokets扩展,可以下载相同版本的源码,进入ext/sockets使用phpize编译安装. socket系列函数 soc