TCP编程
基于大脑遗忘特性,还有就是周五有家公司电话面试我,我一开始给回答错了的原因,在这儿呢,我再强调和复习一下TCP/IP模型。
TCP/IP模型共计 4 层,与OSI模型的七层不一样,TCP/IP更简单和高效。
那我们说OIS模型复杂一下,那么我们先说说OSI模型有哪些吧?
1. OSI模型:
- 物理层 ————————干嘛的?用网线什么的将两个台电脑连起来,然后通过高低电频传递0/1电信号
- 数据链路层————————干嘛的?无规则的
1000011
看不懂,甚至可能还有错误,怎么搞?链路层的作用来了。
数据链路层将无规则的'011000011'
进行协议检测,数据对,则通过且打包成帧,不对,重新发。
还需要控制发送方的发送速率,保证通过链路层的数据无差错地传递 - 网络层 ————————干嘛的?为网络上的不同主机提供通信。具体地说,数据链路层的数据在这一层被转换为数据包,
然后通过路径选择、分段组合、流量控制、拥塞控制等将信息从一台网络设备传送到另一台网络设备。 - 传输层 ————————干嘛的?前三层是数据通信,后三层是数据处理。运输层是第四层,承上启下。网络层只是传到了各个主机(网络设备)
运输层还要控制到具体的程序,所以:第一,提供可靠的端到端(进程到进程)的通信;第二,向会话层提供独立于网络的运输服务。 - 会话层 ————————干嘛的?管理主机之间的会话进程,即负责建立、管理、终止进程之间的会话。
会话层还利用在数据中插入校验点来实现数据的同步。 - 表示层 ————————干嘛的?表示层对上层数据或信息进行变换以保证一个主机应用层信息可以被另一个主机的应用程序理解。
表示层的数据转换包括数据的加密、压缩、格式转换等。 - 应用层 ————————干嘛的?应用层为操作系统或网络应用程序提供访问网络服务的接口。
OSI模型我们已经讲完了,虽然包括我在内的很多人一时之间也记不准,甚至还容易弄混乱,但多复习,总有一天会好的。
接下来,我们讲讲 TCP/IP模型:
2.TCP/IP模型:
- 网络接口层————————干嘛的?负责将二进制流转换为数据帧,并进行数据帧的发送和接收.
- 网络层 ————————干嘛的?负责在主机之间的通信中选择数据包的传输路径,即路由。网络层还要负责处理传入的数据包、
检验其有效性,最后还需要根据需要发出还是接收ICMP差错和控制报文 - 传输层 ————————干嘛的?负责实现应用程序之间的通信服务,又叫端到端通信。传输层要系统的管理信息的流动、还要提供
可靠的传输协议,确保数据到达无差错、无序乱。 - 应用层 ————————干嘛的?把封装好的数据提交给传输层或者是从传输层接受数据并处理。
小结一下:我们看TCP/IP模型可以看出,TCP/IP的网络接口层对应OSI模型的物理层和数据链路层,网络层对应OSI的网络层, 传输层对应传输层,应用层对应OSI的会话层、表示层、应用层。
思考: 在前面的OSI模型中的数据链路层中,我们讲到链路层的作用,
在网络层中我们提到路由的概念,那我们常听到的交换机和路由器到底在模型中的哪一层呢?
(我之前听过多次,但没有接触过交换机,家里也在用路由器,但真没了解过,面试问到都是自己推理出来的,现在我们去了解一下,当然,也只是粗略的说说原理,具体的还是后面再去深究,感觉并不简单。)
-
首先,我们需要弄清楚交换机是什么?路由器是什么?
1. 交换机:https://baike.baidu.com/item/交换机/103532
2. 路由器:https://baike.baidu.com/item/路由器
然后根据《计算机网络自顶向下方法》中扩述,我进行理解为交换机主要在于数据交换,即使现在很多多层交换机,
但交换机的主要作用还是在于特定网络内的数据交换,路由器主要在于寻址路由,实现不同网络之间的数据转发。 -
路由器和交换机位于什么位置?
路由器位于网络层,毕竟主要是实现最佳路由的数据转发。
交换机得分情况,据《计算机网络自顶向下方法》P15
介绍交换机分:
1.路由器;
2.链路层交换机。
路由器不再讲,链路层交换机当然就是在数据链路层了,位于TCP/IP模型的网络接口层。 -
路由和交换机的原理和具体过程,本次就暂不介绍了,我自己也还不清楚,有时间了我再好好学习,整理整理发出来。
3. socket编程的基本函数
-
socket()
:创建套接字,同时制定协议和类型 -
bind()
:绑定本机 (IP + port
) + 套接字,主要用于服务器,客户端不需要绑定。 -
listen()
:设置监听,将套接字设置为监听模式,准备接受客户端的连接请求 -
accept()
:接收TCP
,服务器调用accept()
等待接收客户端的连接,建立好后返回一个新的连接套接字 -
connect()
:建立连接,客户端通过该函数向服务器的监听套接字发送连接请求 -
send()
:TCP
发送数据,也可以用在UDP
中 -
recv()
:TCP
接收数据,也可以用在UDP
中 -
sendto()
:UDP
发送数据,当用在TCP
时候,地址参数失效,等同于send()
-
recvfrom()
:UDP
接收数据,当用在TCP
时候,地址参数失效,等同于recv()
-
close()
:关闭套接字备注:通常来讲,服务器是固定的,方便客户端的连接。
4. 实现思路
想一想,假设是 TCP
通信,客户端和服务器从创建socket()
到 close()
,这中间由什么异同吗?
1. 服务器一般是长时间开启,那么基本是固定的。只需要等待客户端的连接,连接成功后处理事务。
2. 客户端是多样的,那么客户端就需要去主动连接服务器,在这个中间,就需要考虑怎么建立连接问题。
3. 既然客户端是移动多变的IP, 服务器是固定的IP和port,那么到底谁绑定谁?有没有谁可以不绑定呢?
4. 首先,服务器和客户端肯定都需要创建socket的。
5. 创建好了后,服务器需要绑定本机,就是服务器自己的信息,为什么必须绑定呢?那客户端需不需要去绑定自己呢?
1. 服务器不绑定自己 IP
和端口,客户端怎么能够找到服务器呢?所以服务器必须绑定
2. 客户端可以不需要绑定,系统会自动分配端口,连接服务器是客户端的主动过程,在这个过程中客户段系统自动
分配一个端口,随着连接信息发送给服务器,服务器收到信息后拆解其中的IP和端口就知道服务器了,就可以正常
通信了,如果绑定,端口号反而容易出错,所以一般主观上客户端不绑定,让系统自动分配。
6. 客户端是一个主动的过程,服务器是一个被动的过程,那么,服务器就需要提前开启自己,让自己保持在
随时可以连接的状态,即 listen
状态,那么服务器就需要创建 listen(),
然而客户端不需要。
7. 前面总共几个步骤了?
服务器 ------------------------------------ 客户端
socket() -
--------------------------------- socket()
bind()
listen()
8. 经过前面的步骤,服务器就处于一个等待连接的状态了,客户端也处于已经创建好的状态,随时可以连接。
那么,在这儿,客户端进行主动行连接,即 connect()
,服务器是被连接的,需要接受信息,所以 accept()
9. 现在客户端和服务器就连接好了,只需要发送信息和接收信息了。所以双方都是send()、recv()
。
10. 信息发完了,服务器和客户端各自关闭close()。这儿,需要考虑谁先关谁后关的问题。
一般来讲服务器是长时间开启且固定,那么正常情况下就是客户端先关闭,开启四次挥手过程。
如果服务器先关闭了,那么服务器并不处于listen状态,客户端的报文将无法被服务器接收,
会返回一个错误值给客户端,客户端在收到错误值的时候,也会自动关闭TCP。
整理一下:
服务器 ----------------------------------客户端
socket() ----------------------------- socket()
bind()
listen()
accept() -------------------------------connect()
send()/recv()------------------------recv()/send()
recv()/send() -----------------------send()/recv()
close() -------------------------------close()
7 步骤 --------------------------------- 5 步骤
当连接建立好后,服务器和客户端都可以主动发送信息,不一定非要客户端先发送。
5. 函数重点讲解:
-
socket():
int socket( int family, int type, int protocol)
family:协议家族 type:套接字类型 protocol:0(原始套接字除外:ping命令等)-
family协议家族:1. AF_INET -----IPV4协议
2. AF_INET6 -----IPV6协议
3. AF_LOCAL -----UNIX或协议(本地协议)
4. AF_ROUTE -----路由套接字
5. AF_KEY -----密钥套接字 -
type套接字类型:1. SOCK_STREAM ------流式套接字( TCP)
2. SOCK_DGRAM ------数据报套接字( UDP)
3. SOCK_RAW ------原始套接字( ping命令)返回值: 1. 成功: 非负套接字描述符(文件描述符) 2.出错:-1
帮助手册" man socket"看一下,AF_INIT(IPV4)在第七页," man 7 ip"试试。
进入后,选择 tcp_socket = socket( AF_INET, SOCK_STREAM, 0);
ipv4 的TCP连接创建套接字
-
-
bind():
int bind( int sockfd, struct sockarrd * my_addr, int addrlen)-
sockfd:套接字描述符(socket()返回值)
-
my_addr:绑定的地址
-
addrlen:地址的长度
-
struct sockaddr * my_addr为通用型,需要将实际型转换为通用型。怎么转呢?不要急。
当为服务器时候, struct sockaddr *serveraddr(my_addr就是一个名字而已,自己取)帮助手册 " man bind"看看, int bind( int sockfd, struct sockarrd * my_addr, int addrlen)
在 " DESCRIPTION"去看:原文:
When a socket is created with socket(2), it exists in a name space (address family) but has no
address assigned to it. bind() assigns the address specified to by addr to the socket referred
to by the file descriptor sockfd.
addrlen specifies the size, in bytes, of the address structurepointed to by addr.
Traditionally, this operation is called “assigning a name to a socket”.It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may
receive connections (see accept(2)).The rules used in name binding vary between address families. Consult the manual entries in Sec‐
tion 7 for detailed information. For AF_INET see ip(7), for AF_INET6 see ipv6(7), …
翻译:
使用socket(2)创建套接字时,该套接字存在于名称空间(地址族)中,但未分配地址。
bind()将addr指定的地址分配给文件描述符sockfd所引用的套接字。
addrlen指定addr指向的地址结构的大小(以字节为单位)。
传统上,此操作称为“为套接字分配名称”。在SOCK_STREAM套接字接收连接之前,通常必须使用bind()分配本地地址(请参见accept(2))。
名称绑定中使用的规则在地址族之间有所不同。有关详细信息,请查阅第7节中的手册条目。
对于AF_INET,请参见ip(7);对于AF_INET6,请参见ipv6(7)…注意: 看帮助手册中“ 对于AF_INET,请参见ip(7)”,所以还是回到了帮助" man 7 ip".
在 " DESCRIPTION"去看:原文:
When a process wants to receive new incoming packets or connections,
it should bind a socket to a local interface address using bind(2).
Only one IP socket may be bound to any given local (address, port) pair.
When INADDR_ANY is specified in the bind call, the socket will be bound to all local interfaces.
When listen(2) or connect(2) are called on an unbound socket,
it is auto‐ matically bound to a random free port with the local address set to INADDR_ANY.翻译:
当进程想要接收新的传入数据包或连接时,应使用bind(2)将套接字绑定到本地接口地址。
任何给定的本地(地址,端口)对都只能绑定一个IP套接字。
在bind调用中指定INADDR_ANY时,套接字将绑定到所有本地接口。
在未绑定的套接字上调用listen(2)或connect(2)时,
它将自动绑定到本地地址设置为INADDR_ANY的随机空闲端口。接下来。我们继续看 Address Format( 地址格式)
原文:
An IP socket address is defined as a combination of an IP interface address and a 16-bit port number.
The basic IP protocol does not supply port numbers,
they are implemented by higher level protocols like udp(7) and tcp(7).
On raw sockets sin_port is set to the IP protocol.struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET /
in_port_t sin_port; / port in network byte order /
struct in_addr sin_addr; / internet address */
};/* Internet address. /
struct in_addr {
uint32_t s_addr; / address in network byte order */
};sin_family is always set to AF_INET.
This is required; in Linux 2.2 most networking functions return EINVAL when this setting is missing.
sin_port contains the port in network byte order The port numbers below 1024 are called privileged ports (or sometimes: reserved ports).
Only privileged processes (i.e., those having the CAP_NET_BIND_SERVICE capability) may bind(2) to these sockets.
Note that the raw IPv4 protocol as such has no concept of a port,
they are only implemented by higher protocols like tcp(7) and udp(7).sin_addr is the IP host address.
The s_addr member of struct in_addr contains the host interface address in network byte order.
in_addr should be assigned one of the INADDR_* values (e.g INADDR_ANY) or set using the inet_aton(3),
inet_addr(3), inet_makeaddr(3) library functions or directly with the name resolver (see gethostbyname(3)).翻译:
地址和16位端口号的组合。基本IP协议不提供端口号,而是由更高级别的协议(如udp(7)和tcp(7))实现的。在原始套接字上,将sin_port设置为IP协议。struct sockaddr_in {
sa_family_t sin_family; / * 地址族:AF_INET * /
in_port_t sin_port; / * 网络字节顺序的端口 * /
struct in_addr sin_addr; / * 互联网地址 * /
};/ *互联网地址。 * /
struct in_addr {
uint32_t s_addr; / * 地址以网络字节顺序 * /
};sin_family始终设置为AF_INET。
这是必需的;在Linux 2.2中,缺少此设置时,大多数联网功能都会返回EINVAL。
sin_port包含网络字节顺序的端口。低于1024的端口号称为特权端口(有时称为保留端口)。
只有特权进程(即具有CAP_NET_BIND_SERVICE能力的进程)可以将(2)绑定到这些套接字。
请注意,这样的原始IPv4协议没有端口的概念,它们仅由更高的协议(如tcp(7)和udp(7))实现。sin_addr是IP主机地址。 struct in_addr的s_addr成员包含网络字节顺序的主机接口地址。
应该为in_addr分配INADDR_ *值之一(例如INADDR_ANY),或者使用inet_aton(3),
inet_addr(3),inet_makeaddr(3)库函数或直接使用名称解析器进行设置(请参阅gethostbyname(3))。注意:
我们这儿搞这么多,其实就是为了my_addr,这个地址怎么去表示,填写。在 " man 7 IP"中Address Format( 地址格式)
中可以看到 my_addr的表示是一个结构体。my_addr 结构体含有三个成员: 1. " sin_family"(协议族,IPV4(AF_INET)还是IPV6(AF_INET6)之类的)
2. " sin_port"(网络字节序端口)
3. " sin_addr"(互联网地址)
这儿需要注意: 1. sin_family = AF_INET(即可表示采用IPV4协议)
2. sin_port是网络字节序端口,需要进行转化为大端存储,16位htons(端口号:8888)
这儿需要特别注意,初始化填写" 8888"为整形数字,
如果是初始化时候没填写,后期手动写的"8888"为字符串,需要进行转化为整形,atoi()
3. " sin_addr"对应的是一个结构体,需要去具体的看结构体in_addr 中只有s_addr一个成员,表示:地址以网络字节序顺序存储。
假设服务器 IP:192.168.2.219,则表达方式 sin_addr.s_addr = inet_addr(“192.168.2.219”)
在网络通信中,统一规定:数据以高位字节优先顺序在网络上传输————————大端存储。综合:
申明变量、创建套接字、绑定IP 与 端口
struct sockaddr_in serveraddr;
int sockfd = socket( AF_INET, SOCK_STREAM, 0);原型:
int bind( int sockfd, struct sockarrd * my_addr, int addrlen)
实际:
bzero( &serveraddr, sizeof(serveraddr)); // 将空间元素值置 0;
serveraddr.sin = AF_INET; // 设置IP协议为 IPV4;
serveraddr.sin_port = htons(8888); // 绑定本地端口(8888);
serveraddr.sin_addr.s_addr = inet_addr(" 192.168.2.219") // 绑定本地IP (192.168.2.219)
bind( sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); // 实际绑定
-
-
listen():
int listen( int sockfd, int backlog)
sockfd : 套接字描述符
backlog: 请求队列中方允许的最大数,大多数默认5. // 监听连接的套接字这个函数很好理解,不多讲。
listen( sockfd, 5);
注意:
listen()函数不会阻塞,它主要做的事情为,
将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,
将建立好的链接自动存储到队列中,如此重复。
所以,只要 TCP 服务器调用了 listen(),
客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成。
https://i.youkuaiyun.com/#/uc/collection-list -
accept():
int accept( int sockfd, struct sockaddr * addr, socklen_t * addrlen)
sockfd : 套接字描述符(服务器的socket描述符sockfd)
addr : 用于保存用户客户端地址
addrlen: 地址长度
返回值 : 1. 成功:建立好连接的套接字描述符 (新的套接字) 2. 出错:-1注意:
1.返回值的新描述符是已经连接的描述符,表示和客户端已经连接好了。2.addr:指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址
实际参数格式由套接字创建时所产生的地址族决定
可选,即可置为 NULL;3.addrlen: 指针,输入参数,配合addr一起使用,指向存有addr地址长度的整形数
可选,即可置为 NULL;理解: 有人通过你正在监听listen()的端口连接connect()到你,他的连接将加入等待接受accept()
的队列中去,你用accept()告诉他你有资源可以连接,他讲返回一个新的描述符。这样就存在两个描述符了。
原来的继续监听listen()原来的端口,新的描述符用来接收和发送信息。这个新生成的套接字在《计算机网络自顶向下方法》中P110被称为“连接套接字”,原始套接字叫“欢迎套接字”。
-
send():
int send( int sockfd, const void * buf, int len, int flags)
sockfd: 套接字描述符(),这个描述符是accept()的返回值描述符,不是socket()描述符
buf :发送缓冲区的地址, 其实就是弄个地方缓存一下
len : 发送数据的长度,不是缓冲区的长度,是实际要发送的长度
flags : 一般为 0; <具体为什么是0,我目前也还没弄清楚,弄清楚了再补上>
目前所知在《TCP IP网络编程》中描述为"传输数据时用到的多种选项信息",“0代表着不需要设置任何选项”,
返回值: 1.成功: 实际发送的字节数 2.失败:-1 -
recv():
int recv( int sockfd, void * buf, int len, unsigned int flags)
sockfd: 套接字描述符,连接套接字
buf : 存放接收数据的缓冲区
len : 接收数据的长度
flags : 一般为0
返回值: 1.成功:实际接收的字节数 2.失败:-1 -
sendto():
int snedto( int sockfd, const void * buf, int len, unsigned int flags, const struct sockaddr * to, int tolen)
sockfd: 套接字描述符
buf : 发送缓冲区首地址
len : 发送数据的长度
flags : 一般为0,调用方式标志位,改变数值则改变sendto()方式
to : 接收方套接字的 IP和端口号
tolen : 地址长度
返回值: 1. 实际发送的字节数 2.失败:-1 -
recvfrom():
int recvfrom( int sockfd, void * buf, int len, unsigned int flags, struct sockaddr * from, int * fromlen)
sockfd: 套接字描述符,accept()返回的新的套接字
buf : 存放接收数据的缓冲区
len : 数据长度
flags : 一般为 0
from :发送发的IP地址和端口号信息,如果不需要知道,可设置为NULL
fromlen: 地址长度,可设置为NULL
返回值: 1. 成功: 实际接收到的字节数 2. 出错:-1
recvfrom()的返回值一般不为0,因为没有连接,即使断开也不会进行提醒。
6. 服务器模型
1、循环服务器
循环服务器,循环的是什么?
循环服务器模型指服务器依次处理每个客户端、直到当前客户端的所有请求处理完毕,再处理下一个客户端。
所以循环接收、处理、发送,不需要也不是循环绑定、监听
比如三个人问老师问题,老师是服务器,采取的措施是解决完一号所有问题解决二号的,以此类推
注意:循环中的循环,循环嵌套。外层循环依次提取每个客户端的连接请求,建立TCP连接。
内层循环接收并处理当前客户端的所有数据,直到客户端关闭连接。
如果当前客户端没有处理结束,其他客户端必须一直等待。
循环服务器:
{
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);
while(1)
{
recv(...);
process(...);
send(...);
}
close(...);
}
}
2、并发服务器
什么是并发?简单点说就是在这个时间内有多个程序在这个处理机上运行。
并发服务器的基本思想就是采取多任务机制(多进程/线程),分别为每个客户端创建一个任务来处理。
比如三个人同时问老师问题,老师是服务器,需要同时处理三个学生的问题,回答A一个问题回答B一个
并不是 A 所有问题解答结束再解答 B 的问题
注意:这儿需要创建新的进程,在什么时候进行创建呢?谁去处理事务呢?创建完了什么时候关闭呢?
服务器端父进程一旦接受客户端的连接请求,便建立好连接并创建新的子进程。子进程处理客户端事务。
服务器的多个子进程同时运行,处理多个客户端。
服务器的父进程并不处理每个具体的数据请求。
并发服务器:
{
listenfd = socket(...);
bind(...);
listen(...);
signal(SIGCHLD, handler);
while(1)
{
connfd = accept(...);
if ( fork() = = 0)
{
close(listenfd);
while(1)
{
recv(...);
process(...);
send(...);
}
close(connd);
exit(...);
}
close(connfd);
}
void handler(int signo){
while (waitpid(-1, NULL, WNOHANG) > 0); //循环收尸
}
思考: 一个并发服务器,用多进程和多线程来实现,它们存在主要区别是什么?
首先你得回忆一下什么叫进程、线程,有什么区别。
进程:进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程,是操作系统及进行资源分配和调度的基本单位。
线程:线程是进程内独立的一条运行路线,也是内核调度的最小单元,也被称为轻量级进程
一个进程可包括多个线程
多线程:同一时刻执行多个线程。用浏览器一边下载,一边听歌,一边看网页。。。
多进程:同时执行多个程序。如,电脑同时运行微信,QQ,各种浏览器等任务。
多进程优点:
1、每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
2、通过增加CPU,就可以容易扩充性能;
3、可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
4、每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。
多进程缺点:
1、逻辑控制复杂,需要和主程序交互;
2、需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大;
3、最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,
这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……
4、方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。
多线程:
多线程的优点:
1、无需跨进程边界;
2、程序逻辑和控制方式简单;
3、所有线程可以直接共享内存和变量等;
4、线程方式消耗的总资源比进程方式好。
多线程缺点:
1、每个线程与主程序共用地址空间,受限于2GB地址空间;
2、线程之间的同步和加锁控制比较麻烦;
3、一个线程的崩溃可能影响到整个程序的稳定性;
4、到达一定的线程数程度后,即使再增加CPU也无法提高性能。
5、线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU。
7. 常见协议
TCP(Transport Control Protocol)传输控制协议
IP(Internetworking Protocol)网间协议
UDP(User Datagram Protocol)用户数据报协议
ICMP(Internet Control Message Protocol)互联网控制信息协议
SMTP(Simple Mail Transfer Protocol)简单邮件传输协议
SNMP(Simple Network manage Protocol)简单网络管理协议
HTTP(Hypertext Transfer Protocol) 超文本传输协议
FTP(File Transfer Protocol)文件传输协议
ARP(Address Resolution Protocol)地址解析协议