计算机网络教程-套接字 - STEMHA's Blog

计算机网络教程-套接字

套接字的数据结构

C 语言将套接字定义为一个结构(struct)。套接字结构由五个字段组成;每个套接字地址是一个由五部分构成的结构。

图1,套接字数据结构

  • 。这个字段定义了协议簇(如何解释地址和端口号)。
    • 通常值是PF_INET(用于当前因特网)、PF_INET6(用于下一代因特网)等等。我们在本节使用PF_INET。
  • 类型。这个字段定义了四个套接字类型:
    • SOCK_STREAM(用于TCP)
    • SOCK_DGRAM(用于UDP)、
    • SOCK_SEQPACKET(用于SCTP)
    • SOCK_RAW(用于直接使用ISP 服务的应用)。
  • 协议。这个字段定义了族中特定协议。对于TCP/IP 协议簇这个字段设置为0,因为它是族中唯一的协议。
  • 本地套接字地址。这个字段定义了本地套接字地址。
    • 一个套接字地址是一个结构,它由长度字段族字段(对于TCP/IP 协议簇,它被设置为常量AF_INET)、端口号字段(定义了进程)以及IP 地址字段(定义了正在运行的进程所在的主机)构成。它也包含未使用字段。
  • 远程套接字地址。这个字段定义了远程套接字地址。它的结构与本地套接字地址相同。

头文件

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <arpa/innet.h>
#include <sys/wait.h>

使用 TCP 通信

  • TCP 是面向连接的协议。在发送或接收数据之前,需要在客户端和服务器之间建立连接。在连接建立之后,只要它们有数据要发送或接收,两端就可以彼此发送以及接收数据块。
  • TCP 连接可以是迭代的(一次服务一个客户)也可以是并发的(一次服务多个客户)。

TCP 中使用的套接字

TCP 服务器使用两个不同的套接字:

  • 一个用于连接建立。称为监听套接字(listen socket)
  • 一个用于数据传输。称为套接字(socket)
  • 设置两种套接字的目的是将建立阶段和数据交换阶段分开。

服务器使用监听套接字来监听试图建立连接的新客户。在连接建立之后,服务器创建一个用于和客户交换数据的套接字并且最终终止连接。
客户只使用一个套接字用于连接建立以及数据交换。

图2,TCP 通信中使用的套接字

通信流程图

图3,迭代TCP通信流程图

服务器进程

  1. TCP 服务器进程调用socket 和bind 函数,但是这两个函数创建监听套接字,它只在连接建立阶段被使用。
  2. 之后,服务器进程调用listen函数,允许操作系统开始接收客户、完成连接阶段并把他们放入等待被服务的列表。
    • 这个函数也定义了被连接的客户等待列表的大小,这依赖于服务器进程的复杂性,但是通常值为5。
  3. 现在,服务器进程开始循环并且逐一对客户进行服务。
    • 在每次循环中,服务器进程调用accept函数从已连接客户的等待列表中去除一个客户,对其进行服务。
    • 如果列表是空的,那么accept 函数进入阻塞状态直到出现一个客户待服务。
    • 当accept 函数返回,它创建一个新的与监听套接字一样的套接字。
  4. 监听套接字现在移入后台,并且新的套接字成为活动套接字。
  5. 服务器进程现在使用连接建立期间获得的客户套接字地址,用它来填充新建套接字的远程套接字地址。

此时,客户和服务器可以交换数据。我们没有给出数据传输的特定方式,因为这取决于特定的客户-服务器对。

  • TCP 使用send以及recv程序在它们之间传输数据字节。这两个函数比UDP 中使用的sendto 和recvfrom 函数更简单,因为它们不提供远程套接字地址;连接已经在客户和服务器之间建立。
  • 然而,由于TCP 用于传输无边界报文,每个应用需要仔细设计数据传输部分。

send 和recv 函数可能被调用多次来处理大量数据传输。可以将上图的流程图当作一个通用流程图;如果是特殊用途,需要定义服务器数据传输(sever data-transfer)盒。

客户进程

客户进程进行主动开启(active open)。换言之,它开启连接。它调用socket 函数来创建一个套接字并填充前三个字段。
尽管某些实现要求客户进程也调用bind 函数来填充本地套接字,但通常这是由操作系统自动完成的,操作系统为客户选择一个临时端口号
最终close 函数被调用以销毁套接字。
客户流程图与UDP 版本类似,除了客户数据传输(client data-transfer)盒需要为每个特定情况定义。

套接字接口编程(TCP)

编写客户和服务器程序来模拟使用TCP的标准回送应用——客户程序发送一个短的字符串给服务器;服务器将相同的字符串回送到客户。在我们这样做之前,需要为客户和服务器数据传输盒提供流程图

客户和服务器数据传输盒的流程图

图4,客户和服务器数据传输盒的流程图
(发送消息和回送消息)对于发送和回送短的字符这个特定的情况,因为待发送的字符串很短(小于几个单词),我们可以在客户端调用send函数一次完成。
然而,TCP 并不保证把整个报文在一个报文段内发送。因此,我们需要在服务器端调用一组recv(在一个循环内)来接收整个报文并将它们收集到缓冲区内,从而能一次性发送回去。
当服务器向客户发送回送报文时,它也可能使用多个报文段,这意味着客户的recv 程序需要调用多少次就会被调用多少次。

缓冲区设置

另一个有待解决的问题是设置缓冲区,缓冲区用于在每个站点接收数据。

  • 我们需要控制接收的字节数以及下一个数据块存储的位置。
  • 如图所示,程序设置了一些变量进行控制。
  • 在每次迭代中,指针(ptr)移动指向下一个要接收的字节,接收字节的长度(len)呈增长趋势并且待接收的最大字节数(maxLen)呈减少趋势。

图5,用于接收的缓冲区

回送服务器程序

程序遵循迭代TCP通信流程图。
第 6 行到第16 行声明并定义了变量。
第18 行到第21 行分配内存并且按UDP 情况下所述创建了本地(服务器)套接字地址。
第23 行到第27 行创建了监听套接字。第29 行到第33 行将监听套接字绑定到第18 行到第21 行创建的服务器套接字地址上。
第35 行到第39 行是TCP 通信中的新内容。调用 listen 函数让操作系统完成连接建立阶段并将客户置入等待列表。
第44 行到第48 行调用accept 函数来移除等待列表中的第一个客户并开始为其服务。如果在等待列表中没有客户,那么这个函数处于阻塞状态。
第50 行到第56 行对图2-63 中描述的数据传输部分进行编码。最大缓冲区大小与回送字符串长度都和图5中所示相同。

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
//回送服务器程序
# include"headerFiles.h" //也就是我上面写的那些头文件的集合
int main()
{
//声明并定义
int ls; //监听套接字描述符(引用)
int s; //套接字描述符(引用)
char buffer[256]; //数据缓冲区
char *ptr=buffer; //数据缓冲区
int len=0; //等待接收或者发送的字节数
int maxLen=sizeof(buffer); //最大接收字节数
int n; //每次调用receive接收的字节数
int waitSize=16; //等待客户数量
struct sockaddr_in servAddr; //服务器地址
struct sockaddr_in clntAddr; //客户地址
int clntAddrLen; //客户地址长度
//创建本地(服务器)套接字地址
memset(&servAddr,0,sieof(servAddr));
servAddr.sin_family=AF_INET;
servAddr.sin_addr.s_addr=htonl(INADDR_ANY); //默认IP地址
servAddr.sin_port=htonl(SERV_PORT); //默认端口
//创建监听套接字
if(ls=socket(PF_INET,SOCK_STREAM,0)<0) //PF_INET族,SOCK_STREAM指套接字类型(tcp)
{
perror("Error:Listen socket failed!");
exit(1);
}
//将套接字绑定到本地套接字地址
if(bind(ls,&servAddr,sizeof(servAddr))<0)
{
perror("Error:binding failed!");
exit(1);
}
//创建连接请求
if(listen(ls,waitSize)<0)
{
perror("Error:listening failed!");
exit(1);
}
//处理连接
for(;;) //永远运行
{
//接收来自客户的连接
if(s=accept(ls,&clntAddr,&clntAddrLen)<0)
{
perror("Error:accepting failed!");
exit(1);
}
//数据传输部分
while((n=recv(s,ptr,maxLen,0))>0)
{
ptr+=n; //在缓冲区上移动指针
maxLen-=n; //调整待接收的最大字节数
len+=n; //更新已经接收的字节数
}
send(s,buffer,len,0); //发回(回送)所有接收的字节
//关闭套接字
close(s);
}//循环结束
}//回送服务器程序结束

tips:

  • 在C/C++写网络程序的时候,往往会遇到字节的网络顺序和主机顺序的问题。这是就可能用到htonl(), ntohl(), ntohs(),htons()这4个函数。
  • htonl()–”Host to Network Long”
  • ntohl()–”Network to Host Long”
  • htons()–”Host to Network Short”
  • ntohs()–”Network to Host Short”

回送客户程序

TCP 的客户程序与UDP 的客户程序非常相似,只有些许不同。

  • 因为TCP 是面向连接的协议第36 行到第40 行调用connect 函数连接服务器。
  • 第42 行到第48 行使用图3中的思想完成数据传输。
  • 按图5所示方式完成接收数据的长度调整和指针移动。
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
//回送客户程序
# include"headerFiles.h" //也就是我上面写的那些头文件的集合
int main(int argc,char *argv[]) //三个参数之后待检验
{
//声明并定义
int s; //套接字描述符
int n; //每次调用recv接收的字节数
char servName; //服务器名
int servPort; //服务器名
char *string; //被回送的字符串
int len; //被回送的字符串的长度
char buffer[256+1]; //缓冲区
char *ptr=buffer; //在缓冲区上移动指针
struct sockaddr_in serverAddr; //服务器套接字地址
//检测并设置参数
if(argc!=3)
{
printf("Error:three arguments are needed!");
exit(1);
}
servName=argv[1];
servPort=atoi(argv[2]);
string=arg[3];
//创建远程(服务器)套接字地址
memset(&serverAddr,0,sieof(serverAddr));
servAddr.sin_family=AF_INET;
inet_pton(AF_INET,servName,&serverAddr.sin_addr); //服务器IP地址
serverAddr.sin_port=htons(SERV_PORT); //默认端口
//创建套接字
if(ls=socket(PF_INET,SOCK_STREAM,0)<0)
{
perror("Error: socket creation failed!");
exit(1);
}
//连接到服务器
if(connect(sd,(struct sockaddr*)&serverAddr,sizeof(serverAddr))<0)
{
perror("Error:connection failed!");
exit(1);
}
//数据传输部分
send(s,string,strlen(string),0);
while((n=recv(s,ptr,maxLen,0))>0)
{
ptr+=n; //在缓冲区上移动指针
maxLen-=n; //调整待接收的最大字节数
len+=n; //更新已经接收的字节数
}
//打印并验证回送的字符串
buffer[len]='\0';
printf("Echoed string received:");
fputs(buffer,stdout);
//关闭套接字
close(s);
//停止程序
exit(0);
}//回送客户程序结束

Linux下的socket()函数

inux中的一切都是文件,每个文件都有一个整数类型的文件描述符
socket也是一个文件,也有文件描述符。使用socket()函数创建套接字以后,返回值就是一个 int类型的文件描述符
在 Linux 下使用 <sys/socket.h> 头文件中 socket()函数来创建套接字,原型为:

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

1) af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。

  • AF 是“Address Family”的简写,INET是“Inetnet”的简写。
  • AF_INET 表示 IPv4 地址,例如 127.0.0.1;
  • AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
  • 也可以使用 PF 前缀,PF 是“Protocol Family”的简写,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。

需要记住127.0.0.1,它是一个特殊IP地址,表示本机地址

2) type 为数据传输方式/套接字类型,

  • 常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字)
  • SOCK_DGRAM(数据报套接字/无连接的套接字)

3) protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?

正如大家所想,一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

本教程使用 IPv4 地址,参数 af 的值为 PF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() 函数:

1
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议

这种套接字称为 TCP 套接字。

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP,因此可以这样来调用 socket() 函数:

1
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议

这种套接字称为 UDP 套接字。

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:

1
2
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字

Windows下的socket()函数

Windows 会区分 socket 和普通文件,它把 socket 当做一个网络连接来对待,调用 socket() 以后,返回值是 SOCKET 类型,用来表示一个套接字。
Windows 下也使用 socket() 函数来创建套接字,原型为:

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

除了返回值类型不同,其他都是相同的。Windows 不把套接字作为普通文件对待,而是返回 SOCKET 类型的句柄。

参考资料

计算机网络-自顶向下方法
bind()和connect()函数:绑定套接字并建立连接
listen()和accept()函数:让套接字进入监听状态并响应客户端请求
send()/recv()和write()/read():发送数据和接收数据
TCP协议的无消息边界问题
TCP协议的粘包问题(数据的无边界性)
TCP网络传输“粘包”问题,经典解决(附代码)
socket()函数用法详解:创建套接字

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×