文章目录
- 1. 网络中标识进程
- 2. 网络字节序
- 3. socket编程的常见接口
- (1) ` int socket(int domain, int type, int protocol);`
- (2) `int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);`
- (3) `ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);`
- (4)` ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);`
- (5) `int listen(int sockfd, int backlog);`
- (6)`int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);`
- (7) ` int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);`
- 4. UDP协议和TCP协议简单认识
- 5. UDP网络程序
- 6. TCP网络编程
- 7. 总结上述的网络编程
- 8. TCP和UDP的对比
前言: TCP/IP 五层协议中,今天讲的是应用层。我们通过系统调用来实现简单的网络通信,虽然原理还不是那么的清楚,但是 不影响我们去使用,就像开车一样,虽然我们不懂车的构造原理等,但是照样可以开车。今天的主要任务是完成UDP和TCP两个版本的网络通信。
1. 网络中标识进程
在操作系统中,标识进程的是PID,网络中呢?首先网络中是多台机器,必须要先得标识一台主机;其次标识主机中的某个请求网络的进程。
1.1 IP地址
IP地址是用于网络中标识主机的,在网络中标识唯一的一台主机。IP数据包头中包含源IP地址和目的IP地址。也就是从哪台主机来,到哪台主机去。
- 源头IP地址:发出数据的主机
- 目的IP地址:接收数据的主机
IP地址解决了两台物理机器之间的相互通信,但是光靠IP地址不能满足要求。
举个例子:有了IP地址后,两台手机可以用蓝牙直接互相传送数据,但是 两台手机想用QQ交流,或者想用抖音交流 光靠蓝牙链接找到彼此,可以吗?答案是不可以,必须找到具体是哪个软件要进行网络请求。
1.2 端口号
- 端口号是用于标识主机中请求网络的进程。
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
端口号具有唯一性,它标识了主机中的 一个进程,也就是说 一个端口号只能标识一个进程。
但我有两个问题:
- 一个进程可以被多个端口号标识吗?
答案是可以。一个进程可以关联多个端口号,但是一个端口号只能关联一个进程。 - 已经有进程PID,为什么还要有端口号?
这是大佬的设计思维,PID相当于身份证号,端口号可以理解成学生学号,这是一种解耦。假如网络也用的是PID,那么PID变化了,是不是也会影响到网络?所以 为了 使这种互相影响减小 就有了端口号;与此同时,端口号 是专门用于网络中的,所以也好操作。
传输层协议(TCP和UDP)的数据段中有两个端口号:
- 源端口号:请求网络发送数据的进程的端口号
- 目的端口号:接收数据的进程的端口号
源端口号和目的端口号,通俗的说 就是 谁来发数据,谁要接收数据。
1.3 小结
IP地址 + 端口号 就能标识 一台主机上的一个进程 。那么 网络其实 就是 进程间的通信
。一台主机里的进程通信 是 被这台主机的操作系统管理的。网络上的进程通信,是 多台主机 上的进程 进行通信,可以把网络看成一个大的操作系统。
2. 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
有大端机,有小端机。如果不加以规定,那么会导致一个现象,小端机的内容发到大端机上,大端机接收的数据是相反的,同理 大端机发到小端机 也是如此。
所以 需要规定 发到网络中的一律按照大端字节序。大端机或是小端机 从网络中取出 一定是大端字节序。所以 对于大端机 就不需要转换,小端机得转换一下。
上述的工作,靠人 是可以自己完成的,但是容易出错。所以管它是大端机还是小端机,统统调用提供的函数接口就好了。
2.1 网络字节序和主机字节序的转换
这四个接口很好记:
- h -> host :可以理解为 计算机主机
- n -> network : 网络的意思
现在看那几个接口 hton 或是 ntoh ,就是主机转网络,网络转主机。
所以从网络中取 需要 转换 往网络中输送 需要 转换
3. socket编程的常见接口
(1) int socket(int domain, int type, int protocol);
用于创建套接字,也就是打开网络文件
函数参数:
- domain -> 协议族,决定了创建的socket的地址类型
- type -> 指定socket的类型
- protocol -> 具体的协议,一般给 0 ,通过前两个参数就能确定是哪个协议
函数返回值:
成功返回的是文件描述符 fd ,也就是创建的文件。失败返回 -1,并设置 errno。
(2) int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用于绑定端口号:
函数参数:
- sockfd -> 创建的socket文件描述符,也就是上面socket()函数的返回值
- addr -> 一个结构体指针,这个结构体有说法,一会讲
- addrlen -> 传的结构体的大小
函数返回值:
成功返回 0 ,失败返回 -1 ,并设置 errno。
首先需要认识到一点:各种网络协议的地址格式不同,但是为了接口的统一,一致都用
struct sockaddr
,传参的时候,需要做一下强转。
因为网络协议的地址格式不同,所以 绑定的结构体肯定也不同,为了统一接口,才设置的struct sockaddr
,比如常用的 strcut sockaddr_in
和struct sockaddr_un
。
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_);
char sun_path[108]; /* Path name. */
};
这就是关于这个第二参数结构体的介绍了。
(3) ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
这是用于接收数据的、
函数参数:
- sockfd -> sock文件描述符
- buf -> 设置一段空间,用于接收 数据
- len -> 上面buf的大小
- flags -> 调用操作的方式 ,设置为 0 即可
- src_addr 输出型参数,可以保存 传来数据方 的结构体
- addrlen 输出型参数 ,可以保持传来数据方 结构体的大小
函数返回值:
成功则返回接收到的字符数,失败返回 -1 。
(4) ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
这是用于发送数据的,
函数参数:
- sockfd -> sock文件描述符
- buf -> 设置一段空间保存要发送的 数据
- len -> 上面buf的大小
- flags -> 调用操作的方式 ,设置为 0 即可
- dest_addr -> 要发送给谁,目的地的结构体
- addrlen -> 结构体大小
函数返回值:
成功返回 发送出的字符数大小,失败返回 -1。
(5) int listen(int sockfd, int backlog);
这个listen函数,用于面向链接的协议,比如TCP协议,像UDP协议就用不到,相当于改变了状态,从不接受别人请求,变成接收别人请求。
它的作用是 将 某个主动套接字变为被动套接字,从而使得一个进程可以接收其他进程的球球,从而成为一个服务器进程。
函数参数 :
就讲讲 第二参数 backlog:规定了内核应该为相应套接字排队的最大连接个数。
先这样理解,要想搞懂第二参数,不是很容易,这不是本章重点
函数返回值:
成功返回 0,失败返回 -1。
(6)int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
这个函数,是用于接收 请求的,上面的listen()是改变状态,变为可以接收请求;accept()函数是 接收请求。
这个函数,使用的时候就明白了。
(7) int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
这个函数用于建立连接,也就是说 它是用于 和 某个进程 连接的。如果它想连接成功,连接的前提就是 被连接的进程是listen状态。
- 服务端:listen() 设置进程可被连接;accept() 接收 连接
- 客户端:connect() 请求建立连接
4. UDP协议和TCP协议简单认识
UDP协议是面向用户数据的,TCP协议是面向连接的。所以UDP不需要建立连接,相反TCP需要建立连接。
- UDP协议:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
- TCP协议:
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
5. UDP网络程序
我们用UDP协议简单的实现: 客户端发送消息,服务端接收消息并打印,然后服务端返回一条消息。
5.1 服务端代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<string>
const uint16_t port = 8080;
int main()
{
struct sockaddr_in my_sock;
// 1,创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
// 2, 绑定 bind
my_sock.sin_family = AF_INET;
my_sock.sin_port = htons(port);
my_sock.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr *)&my_sock, sizeof(my_sock));
// 3,提供服务
char buffer[1024] = {0};
while (true)
{
struct sockaddr_in your_sock;
socklen_t len = sizeof(your_sock);
recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&your_sock, &len);
std::cout << "cilent #" << buffer << std::endl;
std::string s = "hellow 我收到了";
sendto(sock,s.c_str(),s.size(),0,(struct sockaddr*)&your_sock,len);
}
return 0;
}
(1) 创建套接字,也就是打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
(2) 绑定 IP 和 port
// 2, 绑定 bind
const uint16_t port = 8080;
my_sock.sin_family = AF_INET;
my_sock.sin_port = htons(port);
my_sock.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr *)&my_sock, sizeof(my_sock));
协议族sin_family 就是AF_INET,sin_port可以自己定义,我定义的就是 8080 ,但是由于网络字节序,所以还得用 htons(port) 转换一下,IP的话可以填 你云服务器的 IP 但是有可能你的云服务器没有开方,或者多个网卡的情况,所以 直接设置为INADDR_ANY 即可,就相当于发送到此服务器的数据都能接收到。
(3) 提供服务
char buffer[1024] = {0};
while (true)
{
struct sockaddr_in your_sock;
socklen_t len = sizeof(your_sock);
recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&your_sock, &len);
std::cout << "cilent #" << buffer << std::endl;
std::string s = "hellow 我收到了";
sendto(sock,s.c_str(),s.size(),0,(struct sockaddr*)&your_sock,len);
}
buffer用于存客户端传来的消息,recvfrom()是用于接收客户端消息的,服务端发送消息给客户端 用的是sendto()。
有人可能会有疑问:服务端怎么知道给哪个客户端发送消息?
recvfrom() 中输出型参数,就能够取出 客户端的sockaddr_in ,有了它,就能完成sendto()
5.2 客户端代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<stdlib.h>
#include<string>
void usag()
{
std::cout<<"输入格式有误"<<std::endl;
std::cout<<"./UDP_client"<<' '<<"IP"<<' '<<"端口号"<<std::endl;
}
int main(int argv,char* argc[])
{
if(argv < 3)
{
usag();
return 0;
}
// 1, 创建套接字
int sock = socket(AF_INET,SOCK_DGRAM,0);
// 2. 发送数据
struct sockaddr_in send_sock;
send_sock.sin_family = AF_INET;
send_sock.sin_addr.s_addr = inet_addr(argc[1]);
send_sock.sin_port =htons(atoi(argc[2]));
while(true)
{
std::string message;
std::cout<<"请输入 #";
std::cin>>message;
// 发送信息
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&send_sock,sizeof(send_sock));
// 接收信息
struct sockaddr_in s_sock;
socklen_t len = sizeof(s_sock);
char buffer[1024];
recvfrom(sock,buffer,sizeof(buffer),0,(struct sockaddr*)&s_sock,&len);
std::cout<<"server #"<<buffer<<std::endl;
}
return 0;
}
(1) 使用规则
我们期望 客户端使用时,在命令行里就 包括 服务端的IP和port。
比如:
注意 127.0.01
这个IP,是能够完成我们的测试。它是回送地址,指本地机,一般用来测试使用。
(2) 客户端需要注意的就是 取出命令行上的IP 和 port 是字符串,需要做转换
send_sock.sin_addr.s_addr = inet_addr(argc[1]);
send_sock.sin_port =htons(atoi(argc[2]));
- inet_addr() 函数可以将字符串转换为 IP地址。
- atoi() 先把字符串转为整型,再用htons() 把机器字节序转为网络字节序。
5.3 使用UDP网络程序进行通信
同时 我们也可以使用netstat -nlup
指令来查看 客户端和服务端的运行情况:
6. TCP网络编程
TCP协议是面向连接的,所以相较上面的UDP。需要建立连接。
关于这此的网络编程,咱们多整几个版本,分别是单线程(单进程),多进程(父子进程),多线程(线程池)。
6.1 单线程服务端代码
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<cerrno>
void usage()
{
std::cout<<"命令行输入格式有误"<<std::endl;
std::cout<<"./server"<<" "<<"端口号";
}
void ServiceIO(int new_sock)
{
//提供服务,我们是一个死循环
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0; //将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<, ";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if(s == 0){
std::cout << "client quit ..." << std::endl;
break;
}
else {
std::cerr << "read error" << std::endl;
break;
}
}
}
int main(int argv,char* argc[])
{
if(argv != 2)
{
usage();
return 1;
}
// 1. 创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0) {
std::cerr <<"socket error: " << errno << std::endl;
return 2;
}
// 2. bind
struct sockaddr_in my_sock;
my_sock.sin_family = AF_INET;
my_sock.sin_port = htons(atoi(argc[1]));
my_sock.sin_addr.s_addr = INADDR_ANY;
if(bind(sock,(struct sockaddr*)&my_sock,sizeof(my_sock)) < 0)
{
std::cerr <<"bind error: " << errno << std::endl;
return 3;
}
// 3. 设置为listen状态
const int user =5;
if(listen(sock,user) < 0)
{
std::cerr <<"listen error: " << errno << std::endl;
return 4;
}
// 4. 接收连接
while(true)
{
struct sockaddr_in user;
socklen_t size_sockaddr = sizeof(user);
int new_sock = accept(sock,(struct sockaddr*)&user,&size_sockaddr);
if(new_sock < 0)
{
continue;
}
uint16_t cli_port = ntohs(user.sin_port);
std::string cli_ip = inet_ntoa(user.sin_addr);
std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl;
// 以上表明连接过程
// 开始服务
ServiceIO(new_sock);
}
return 0;
}
(1) 首先启用 单线程服务器 ,需要按照这样的格式:
也就是说,我们这次是自定义端口号,如果命令行不按这样的格式输入,我们就直接用usag()函数提醒一下。
(2) 毫无疑问 先得创建套接字:
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0) {
std::cerr <<"socket error: " << errno << std::endl;
return 2;
}
注意第二参数是SOCK_STREAM
,这就是设置为面向连接的套接字。
(3) bind(),绑定
struct sockaddr_in my_sock;
my_sock.sin_family = AF_INET;
my_sock.sin_port = htons(atoi(argc[1]));
my_sock.sin_addr.s_addr = INADDR_ANY;
if(bind(sock,(struct sockaddr*)&my_sock,sizeof(my_sock)) < 0)
{
std::cerr <<"bind error: " << errno << std::endl;
return 3;
}
(4) 设置为listen状态,也就是从不可被连接的状态变为可被连接的状态,这一步代表 当前的进程 变为 一个 服务器
const int user =5;
if(listen(sock,user) < 0)
{
std::cerr <<"listen error: " << errno << std::endl;
return 4;
}
第二个参数是规定了 排队等服务器的最大连接数、
(5) 接收连接
while(true)
{
struct sockaddr_in user;
socklen_t size_sockaddr = sizeof(user);
int new_sock = accept(sock,(struct sockaddr*)&user,&size_sockaddr);
if(new_sock < 0)
{
continue;
}
uint16_t cli_port = ntohs(user.sin_port);
std::string cli_ip = inet_ntoa(user.sin_addr);
std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl;
}
accept()返回的是 请求连接的 文件描述符,所以 就利用文件操作,就能够拿出 客户端发送的数据。
(6) 提供服务
void ServiceIO(int new_sock)
{
//提供服务,我们是一个死循环
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0; //将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<, ";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if(s == 0){
std::cout << "client quit ..." << std::endl;
break;
}
else {
std::cerr << "read error" << std::endl;
break;
}
}
}
6.2 客户端代码
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
#include<stdio.h>
#include<stdlib.h>
void usage()
{
std::cout<<"命令行输入格式有误"<<std::endl;
std::cout<<"./client"<<" "<<"IP"<<"port";
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage();
return 1;
}
// 1. 创建套接字
int sock = socket(AF_INET,SOCK_STREAM,0);
// 2. 连接
// 2.1 连接谁?
struct sockaddr_in send_sock;
send_sock.sin_family = AF_INET;
send_sock.sin_addr.s_addr = inet_addr(argv[1]);
send_sock.sin_port = htons(atoi(argv[2]));
// 2.2 请求连接
if(connect(sock, (struct sockaddr*)&send_sock, sizeof(send_sock)) < 0)
{
std::cout << "connect server failed !" << std::endl;
return 2;
}
std::cout<<"连接成功"<<std::endl;
while(true)
{
std::cout << "Please Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer)-1, stdin);
write(sock, buffer, strlen(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s>0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
需要注意的就两点:
- connect() 函数用于请求连接
- write(),向网络文件中写入数据
6.3 多进程服务端
上面就是一个主线程在服务,比较慢,现实情况基本没有那样使用的,所以我们实现一个多进程版本,也就是 父进程去 接收连接,子进程去 提供服务:
其实就是 服务的方式 的变化,所以 开始服务的代码变为:
// 开始服务
while (true)
{
struct sockaddr_in user;
socklen_t size_sockaddr = sizeof(user);
int new_sock = accept(sock, (struct sockaddr *)&user, &size_sockaddr);
if (new_sock < 0)
{
continue;
}
uint16_t cli_port = ntohs(user.sin_port);
std::string cli_ip = inet_ntoa(user.sin_addr);
std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl;
// 以上表明连接过程
// 开始服务
// 单进程
// ServiceIO(new_sock);
// 多进程
pid_t id = fork();
if (id < 0)
{
continue;
}
else if (id == 0)
{ //曾经被父进程打开的fd,是否会被子进程继承呢? 无论父子进程中的哪一个,强烈建议关闭掉不需要的fd
// child
close(sock);
if (fork() > 0)
exit(0); //退出的是子进程
//向后走的进程,其实是孙子进程
ServiceIO(new_sock);
close(new_sock);
exit(0);
}
else
{
// father,不需要等待
// do nothing!
waitpid(id, NULL, 0); //这里等待的时候会不会被阻塞呢? 不会
close(new_sock);
}
}
6.4 创建一个线程去完成服务
上面是创建一个子进程去完成服务,现在 我们创建一个线程去完成服务:
while(true)
{
struct sockaddr_in user;
socklen_t size_sockaddr = sizeof(user);
int new_sock = accept(sock,(struct sockaddr*)&user,&size_sockaddr);
if(new_sock < 0)
{
continue;
}
uint16_t cli_port = ntohs(user.sin_port);
std::string cli_ip = inet_ntoa(user.sin_addr);
std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl;
// 创建 线程
pthread_t tid;
int * pram = new int(new_sock);
pthread_create(&tid, NULL, HandlerRequest, pram);
}
这里需要提供一个线程的执行方法:
void *HandlerRequest(void *args)
{
pthread_detach(pthread_self());
int sock = *(int *)args;
delete (int*)args;
ServiceIO(sock);
close(sock);
}
6.4 线程池版本-》完成服务
//1. 构建一个任务
Task t(new_sock);
//2. 将任务push到后端的线程池即可
ThreadPool<Task>::GetInstance()->PushTask(t);
首先需要完成一个线程池,这个就不实现了哈。但是逻辑很简单,就是创建任务,然后将任务分配给线程池去处理。
这个关键点在于 任务的创建:
#pragma once
#include <iostream>
#include <cstring>
#include <unistd.h>
namespace ns_task
{
class Task
{
private:
int sock;
public:
Task() : sock(-1) {}
Task(int _sock) : sock(_sock)
{
}
int Run()
{
//提供服务,我们是一个死循环
// while (true)
// {
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; //将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
//拉取逻辑
std::string echo_string = ">>>server<<<, ";
echo_string += buffer;
write(sock, echo_string.c_str(), echo_string.size());
}
else if (s == 0)
{
std::cout << "client quit ..." << std::endl;
// break;
}
else
{
std::cerr << "read error" << std::endl;
// break;
}
// }
close(sock);
}
~Task() {}
};
}
然后将这个任务 push进 线程池 就OK了。
7. 总结上述的网络编程
我们对上面的网络编程做一个总结:
- socket()创建套接字的过程 ,本质就是在打开文件
- bind()绑定,本质是 IP + port 和文件信息进行关联
- listen(),本质就是设置该socket文件的状态,允许别人来连接
- accept(),它就是获取新连接到应用层,返回值是fd 文件描述符
- UDP协议下 进行数据通信用的是 recvfrom() 和 sendto()
- TCP协议下 进行数据通信用的是 read() 和 write(),因为上升到用户层,其实就是对文件的操作
- connect(),本质是发起连接,在操作系统层面就是构建一个请求连接的报文发送过去,在网络层面就是发起 tcp连接的三次握手。这个概念下一篇讲。
- close() ,本质在网络层面就是进行四次挥手,同样下一篇文章讲。
8. TCP和UDP的对比
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流 vs 数据报