本质:端口号是一个2字节16位的无符号整数,范围是[0,65535]
作用:端口号是用来标识一个进程,告诉操作系统,当前的数据要交给那一个进程来处理
注意事项:一个端口只能被一个进程占用
一个进程可以占用多个端口一些知名端口:
[0,1023] 范围内的端口已经被一些知名的协议所使用,我们在编写代码的时候不要使用该范围内的数据作为端口号
MySQL----3306端口
Oracle----1521端口
{源IP、 目的IP 、源端口、目的端口、 协议}
名称 | 作用 |
---|---|
源IP | 标识网络数据是从哪台主机发出的 |
目的IP | 标识数据要去往哪一台主机 |
源端口 | 标识网络数据是从“源IP”对应的这台主机的哪个进程产生的 |
目的端口 | 通过目的IP找到目的主机之后,需要利用目的端口找到对应的进程 |
协议 | 标定双方传输数据时使用的协议,一般是UDP/TCP |
字节序又称为端序或者尾序。指的是多字节数据在内存中存放的顺序。
我们接触的字节序分为两类:小端字节序和大端字节序
他们是两种数据在内存中存放顺序的不同规则
字节序 | 特点 |
---|---|
小端字节序 | 数据的低权值位对应空间的低地址 |
大端字节序 | 数据的低权值位对应空间的高地址 |
如何判断自己的机器遵守的是哪一种存储规则?
方式一:指针+变量+强制类型转换验证
方式二:利用联合体的存储特性来判断
有了上面的铺垫,我们来认识一下主机字节序与网络字节序
主机字节序:指的是机器本身的字节序,如果是大端,则主机字节序为大端,如果是小端,则主机字节序为小端
网络字节序:规定网络传输数据的时候采用大端字节序进行传输
既然网络字节序是大端字节序,现在假设有AB两台主机,他们之间需要通过网络进行通信,我们分析A向B发送消息这一单程。
A向B发送的数据,通过网络传输时一定要转换为网络字节序,否则传输的数据可能会出错(这取决于主机A是大端还是小端机器)
B从网络中接收A发送的数据时,也需要将数据从网络字节序转换为B主机的主机字节序
这个具体的转换过程并不需要我们自己实现,OS为我们提供了转换的接口
服务端
Ⅰ、创建套接字
Ⅱ、绑定地址信息
Ⅲ、收发消息
Ⅳ、使用完毕后关闭套接字客户端
Ⅰ、创建套接字
Ⅱ、不推荐绑定地址信息
不推荐在代码手动绑定地址信息
Ⅲ、收发消息
Ⅳ、使用完毕后关闭套接字
图示:
总结:
1、为什么要创建套接字?
将进程和网卡进行绑定,进程可以从网卡中接收消息,也可以通过网卡发送消息。
2、绑定地址信息具体干了什么?
绑定IP和端口。目的是为了在网络中表示一台主机和一个进程。这样一来,对于接收方而言,发送数据的人就知道接受方的在哪台机器的哪个进程了;对于发送方而言,能够标识网络数据是从哪台机器的哪个进程发送出去的。
#include
int socket(int domain, int type, int protocol)
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
参数:
sockfd-----创建套接字时返回的套接字描述符
addr-----绑定的地址信息(IP + port)
addrlen----绑定的地址信息的长度
注意:这里的 struct sockaddr是一个通用的数据结构!结构如下:
我们在组织参数的时候,传递的并不是上面的这个通用数据结构,而是struct sockaddr_in这个结构体变量,具体内容如下:
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags,const struct sockaddr* dest_addr, socklen_t addrlen);
ssize_t recvfrom (int sockfd, void* buf, size_t len, int flags,struct sockaddr* src_addr, socklen_t* addrlen);
int close(int fd)
服务端
创建套接字
绑定地址信息
监听
获取新连接
收发数据
关闭连接客户端
创建套接字
不推荐绑定地址信息
发起连接
收发数据
关闭连接
图示:
总结:
监听的含义
监听TCP客户端新的连接,同客户端建立TCP连接。(此时,TCP的建立在内核中就完成了)
获取新连接的含义
获取新连接的套接字描述符,每一个TCP连接会产生一个新的套接字描述符
发起连接的含义
向服务端发起连接
int listen(int sockfd, int backlog)
int accept(int sockfd, struct sockaddr* addr, socklen_t * addrlen);
int connect (int sockfd, const struct sockaddr* addr, socklen_t addrlen)
接收数据:
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
注意:返回值为0表示对端关闭连接了,如果此时的对端指的是客户端,则服务端需要将对应的新套接字描述符关闭!
发送数据
ssize_t send(int sockfd, const void* buf, size_t len, int flags)
注意:
服务端在发送数据的时候,第一个参数sockfd传递的是新创建的套接字描述符,并不是侦听套接字的套接字描述符
期望完成的功能:服务端和多个客户端之间能够正常的通信!
服务端主要代码:
客户端主要代码 :
运行结果分析:
分析出现这种情况的原因:
首先,第二个客户端的代码能够执行到提示输入语句处,说明此时该客户端与服务端已经建立了连接。并且客户端发送数据后没有报错,说明客户端数据发送是成功的。因此问题肯定是出在服务端的代码。
我们可以通过命令netstat
来查看当前网络连接状态以及相关信息。
综上所述,我们单线程的TCP代码就目前而言,只能服务于单个客户端的情况。(注意:后续可以通过多路转接IO模型实现服务器与客户端是一对多的现象)
TCP_demo客户端代码
TCP_demo服务端代码
基于这种情况,我们需要找到一种方法,能够同时让多个客户端都享受服务。
因此,我们可以让服务端的一个进程(线程)只负责与客户端建立连接,剩下的一批进程(线程)可以各自与一个客户端进行沟通。这样就可以达到我们的目标了。因此TCP结合多进程/多线程就脱颖而出~
下面分别介绍TCP 与多进程和多线程结合的代码
首先,对于客户端的代码而言,不需要做出任何的改动!因为客户端只需要一直和服务端进行通信即可!
主要的更改是在服务端,我们通过创建子进程的方式来实现职责的分离,也就是父进程只负责与客户端建立连接,而子进程负责与客户端进行收发消息。
具体核心代码如下:
注意几点细节:
1、子进程是拷贝父进程的PCB,因此需要父进程先与客户端建立连接,也即在父进程的PCB中的fd_array中有了该套接字的文件描述符之后再创建子进程
2、子进程创建成功过,由于它只需要和客户端进行收发消息,因此只需要accept返回的新套接字描述符即可,所以需要将拷贝自父进程的侦听套接字关闭
3、客户端如果将连接关闭,则子进程需要将对应的套接字即文件描述符关闭,然后该进程需要退出。
但是注意:退出时,一定要通知父进程来回收子进程的退出状态信息,否则子进程就会编程僵尸进程!但是我们不能采用wait || waitpid来回收。因为wait具有阻塞属性,而waitpid需要搭配循环来使用,均不符合我们的预期。我们可以通过信号量的方式来处理,即改写SIGCHLD信号!
具体代码参考 TCP_process服务端