《TCP/IP网络编程》学习笔记 | Chapter 2:套接字类型与协议设置

《TCP/IP网络编程》学习笔记 | Chapter 2:套接字类型与协议设置

《TCP/IP网络编程》学习笔记 | Chapter 2:套接字类型与协议设置

套接字协议及其数据传输特性

协议(Protocol)

协议就是一种规则,是为了完成数据交换而定的约定。

创建套接字

cpp 复制代码
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

成功返回文件描述符,否则-1。

参数:

  • domain:使用的协议族信息
  • type:数据传输类型信息
  • protocol:通信时用的协议信息

协议族(Protocol Family)

头文件sys/socket.h中声明的协议族:

名称 协议族
PF_INET IPV4互联网协议族
PF_INET6 IPV6互联网协议族
PF_LOCAL 本地通信的UNIX协议族
PF_PACKET 底层套接字的协议族
PF_IPX IPX Novell的协议族

套接字中,实际采用的最终协议信息是通过第三个参数传递的。但是第一个参数决定着第三个参数。

套接字类型(Type)

套接字类型1:面向连接的套接字(SOCK_STREAM)

特征:

  • 传输过程中数据不会消失
  • 按序传输数据
  • 传输的数据不存在数据边界(Boundary)

如何理解数据边界呢?

比如:计算机通过三次write传输了100字节的数据,但是接收数据的计算机仅通过一次read就接收了100字节的数据。

因为收发数据的套接字内部由缓冲(一个字节数组),因此数据也保存到该数组中。因此只要不超过数组容量,read函数和write函数调用次数没什么意义。

即使缓冲已满,也不代表数据丢失。如果read比接受数据的速度慢,则有可能会被填满。此时无法接受数据,但不意味着丢失,因为传输套接字将会停止传输。即面向连接的套接字会根据接收端的状态传输数据,如果传输出错还会重传。因此除特殊情况外,面向连接的套接字不会发生数据丢失。

需要注意:套接字必须一一对应。面向连接的套接字只能与另外一个同样特性的套接字连接。

总的来说,面向连接的套接字是:可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字。

套接字类型2:面向消息的套接字(SOCK_DGRAM)

特征:

  • 强调快速传输而非传输顺序
  • 传输的数据可能丢失也可能损毁
  • 传输的数据有数据边界
  • 限制每次传输的数据大小

总结:不可靠的、不按序传递的、以数据的高速传输为目的的套接字。

协议的最终选择

第三个参数决定最终采用的协议。尽管我们已经通过第一个参数设置协议族信息,第二个参数设置套接字数据传输方式,但是大家要知道:同一协议族中存在多个数据传输方式相同的协议。

所以数据传输方式相同,但是协议不同。

基于IPv4和面向连接,只有IPPROTO_TCP协议满足。该套接字称为TCP套接字。

cpp 复制代码
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

SOCK_DGRAM指的是面向消息的数据传输方式,满足上述条件的协议只有IPPROTO_UDP这种套接字称为UDP套接字。

cpp 复制代码
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

面向连接的套接字:TCP套接字示例

服务端:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char* message);

int main(int argc,char* argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if(argc!=3)
    {
        printf("Usage:%s <IP> <port>\n",argv[0]);
        exit(1);
    }

    sock = socket(PF_INET,SOCK_STREAM,0);
    if(sock==-1)
    {
        error_handling("socket() error!");
    }

    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
    {
        error_handling("connect() error!");
    }

    str_len = read(sock,message,sizeof(message)-1);

    if(str_len==-1)
    {
        error_handling("read() error!");
    }

    printf("Message from server:%s \n",message);
    close(sock);

    return 0;
}


void error_handling(char* message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}

客户端:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main (int argc, char *argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;
    int idx = 0, read_len = 0;

    if(argc != 3){
        printf("Usage: %s <IP> <port> \n", argv[0]);
        exit(1);
    }
    //若前两个参数是PF_INET,SOCK_STREAM则可省略第三个参数
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error");
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if(connect(sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr)) == -1)
        error_handling("connect() error");
//此处每次读取一个字节。
    while(read_len = read(sock, &message[idx++], 1)){
        if(read_len == -1)
            error_handling("read() error");
        str_len += read_len;
    }
    printf("Message from server : %s \n", message);
    printf("Function read call count: %d \n",str_len);
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n',stderr);
    exit(1);
}

从运行结果可以看出,服务端发送了13字节的数据,客户端调用了13次read函数进行读取。由此验证TCP套接字的特性:不存在数据边界。

Windows平台下的实现及验证

因为套接字类型及传输特性与操作系统无关,所以只需要了解返回类型即可。

Windows操作系统的socket函数

Windows和Linux的函数名都相同,只是返回值不同。下面给出声明:

cpp 复制代码
#inclde <winsock2.h>

SOCKET socket(int af, int type, int protocol);

成功返回句柄,否则返回INVALID_SOCKET(实质上是-1)。

基于Windows的TCP套接字示例

服务端:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void ErrorHanding(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hServerSock, hClientSock;
	SOCKADDR_IN serverAddr, clientAddr;
	int szClientAddr;
	char message[] = "Hello World!";

	if (argc != 2)
	{
		printf("Usage: %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHanding("WSAStartup() 	error!");

	hServerSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServerSock == INVALID_SOCKET)
		ErrorHanding("socket() 	error!");

	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServerSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
		ErrorHanding("bind() error!");

	if (listen(hServerSock, 5) == SOCKET_ERROR)
		ErrorHanding("listen() error!");

	szClientAddr = sizeof(clientAddr);
	hClientSock = accept(hServerSock, (SOCKADDR *)&clientAddr, &szClientAddr);
	if (hClientSock == INVALID_SOCKET)
		ErrorHanding("accept() error!");

	send(hClientSock, message, sizeof(message), 0);

	closesocket(hClientSock);
	closesocket(hServerSock);

	WSACleanup();

	return 0;
}

客户端:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

void error_handling(const char *message);

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET hSocket;
    SOCKADDR_IN serverAddr;

    char message[30];
    int strLen = 0;
    int idx = 0, readLen = 0;

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        error_handling("WSAStartup() error");

    hSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET)
        error_handling("hSocket() error");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
    serverAddr.sin_port = htons(atoi(argv[2]));

    if (connect(hSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        error_handling("connect() error");

    while (readLen = recv(hSocket, &message[idx++], 1, 0))
    {
        if (readLen == -1)
            error_handling("read() error");
        strLen += readLen;
        if (message[idx - 1] == '\0')
            break;
    }

    printf("Message from server: %s\n", message);
    printf("Function read call count: %d\n", strLen);

    closesocket(hSocket);
    WSACleanup();
    
    return 0;
}

void error_handling(const char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

编译:

cpp 复制代码
gcc tcp_server_win.c -lwsock32 -o hServerWin

gcc tcp_client_win.c -lwsock32 -o hClientWin

运行结果:

习题

(1)什么是协议?在收发数据中定义协议有何意义?

协议就是为了完成数据交换而定好的规则。

因此,定义协议意味着对数据传输所必需的的规则进行定义。

(2)面向连接的TCP套接字传输特性有3点,请分别说明。

  • 可靠的:传输过程中数据不会丢失
  • 按序传递的:按序传输数据
  • 基于字节的:传输的数据不存在数据边界

(3)下面哪些是面向消息的套接字的特性?

a. 传输数据可能丢失

b. 没有数据边界

c. 以快速传递为目的

d. 不限制每次传递数据的大小

e. 与面向连接的套接字不同,不存在连接的概念

答:a、c、e。

(4)下列数据适合用哪些套接字传输?并给出原因。

a. 演唱会现场直播的多媒体数据

b. 某人压缩过的文本文件

c. 网上银行用户与银行之间的数据传递

答:

a. UDP。演唱会讲究实时性,稍微丢包也能够有算法补救(顶多画质下降),所以用UDP更好。

b. TCP。文本文件讲究可靠传输,所以用TCP。

c. TCP。支付这种敏感数据更需要可靠传输,所以用TCP。

(5)何种类型的套接字不存在数据边界?这类套接字接收数据时需要注意什么?

面向连接的TCP套接字不存在数据边界。因此输入输出函数的响应次数不具有意义。

重要的不是函数的响应次数,而是数据的收发量。需要保证在接收套接字的缓冲区填充满之前就从buffer里读取数据。也就是,在接收套接字内部,写入buffer的速度要小于读出buffer的速度。

(6)tcp_server.c和tcp_client.c中需要多次调用read函数读取服务器调用1次write函数传递的字符串。更改程序,使服务器端多次调用(次数自拟)write函数传输数据,客户端调用1次read函数进行读取。为达到这一目的,客户端需延迟调用read函数,因为客户端要等待服务器端传输所有数据。

服务端:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void error_handling(const char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hServerSock, hClientSock;
	SOCKADDR_IN serverAddr, clientAddr;
	int szClientAddr;
	char message[] = "Hello World!";

	if (argc != 2)
	{
		printf("Usage: %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		error_handling("WSAStartup() error!");

	hServerSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServerSock == INVALID_SOCKET)
		error_handling("socket() error!");

	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(atoi(argv[1]));

	if (bind(hServerSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
		error_handling("bind() error!");

	if (listen(hServerSock, 5) == SOCKET_ERROR)
		error_handling("listen() error!");

	szClientAddr = sizeof(clientAddr);
	hClientSock = accept(hServerSock, (SOCKADDR *)&clientAddr, &szClientAddr);
	if (hClientSock == INVALID_SOCKET)
		error_handling("accept() error!");

	send(hClientSock, message, 4, 0);
	send(hClientSock, message + 4, 4, 0);
	send(hClientSock, message + 8, 4, 0);
	send(hClientSock, message + 12, sizeof(message - 12), 0);

	closesocket(hClientSock);
	closesocket(hServerSock);

	WSACleanup();

	return 0;
}

客户端:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

void error_handling(const char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET hSocket;
    SOCKADDR_IN serverAddr;

    char message[30];

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        error_handling("WSAStartup() error!");

    hSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET)
        error_handling("hSocket() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
    serverAddr.sin_port = htons(atoi(argv[2]));

    if (connect(hSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        error_handling("connect() error!");

    // Busy Waiting
    for (int i = 0; i < 3000; i++)
        printf("Wait time %d\n", i);

    recv(hSocket, message, sizeof(message) - 1, 0);

    printf("Message from server: %s\n", message);

    closesocket(hSocket);
    WSACleanup();

    return 0;
}

运行结果:

相关推荐
Watermelon Y2 小时前
【C++】多线程
c++
南桥几晴秋2 小时前
【算法刷题指南】优先级队列
数据结构·c++·算法·优先队列·大堆·小堆
因特麦克斯2 小时前
C++小问题
c++
C++忠实粉丝2 小时前
计算机网络之传输层协议UDP
linux·网络·c++·网络协议·计算机网络·udp
瘾大侠3 小时前
渗透测试--Linux上获取凭证
linux·网络·安全·web安全·网络安全
爱穿西装的C先生4 小时前
C++学习日记---第16天
开发语言·c++·学习·程序人生·蓝桥杯
烟雨迷4 小时前
类和对象下
开发语言·c++·学习
XLYcmy4 小时前
分布式实验一
linux·c++·分布式·网络安全·操作系统·c·socket
SafePloy安策5 小时前
软件保护:从用户角度出发的安全需求与体验
运维·网络
_小柏_5 小时前
C/C++基础知识复习(36)
c语言·c++