目录
[一、端口号 port](#一、端口号 port)
[二、套接字 socket](#二、套接字 socket)
[1)bind 绑定](#1)bind 绑定)
[2)listen 监听](#2)listen 监听)
[3)accept 接收连接请求](#3)accept 接收连接请求)
[4)recv 接收](#4)recv 接收)
[5)send 发送](#5)send 发送)
[6)connect 连接请求](#6)connect 连接请求)
一、端口号 port
1、为了区分同一主机上的多个进程,使用端口号来进行处理
2、端口号是一个2字节的无符号整数存储,取值范围【0,65535】
3、网络通信中两个"地址",主机的地址------IP,进程的地址------端口号;
4、特殊的端口号:0-1023
由系统默认应用程序占用,编程不可使用
TCP 21端口:FTP文件传输服务
TCP 23端口:TELNET终端仿真服务
TCP 25端口:SMTP简单邮件传输服务
TCP 110端口:POP3邮局协议版本3
TCP 80端口:HTTP超文本传输服务
TCP 443端口:HTTPS加密超文本传输服务
UDP 53端口:DNS域名解析服务
UDP 69端口:TFTP文件传输服务
特殊的端口函数,存储在linux中的 /etc/services文件中
5、编程可使用的:1024-49151
编程可使用的端口号
6、临时端口号:49152-65535
客服端运行时动态选择的,编程时若未指定端口号,会分配临时端口号
二、套接字 socket
相关帮助指令 man 2 socket man 7 socket
1、原理
2、socket函数介绍
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:为通信创建一个端点,并返回该端点的文件描述符
参数1:通信域
Name Purpose Man page
AF_UNIX, AF_LOCAL 本地通信,同一主机之间进程通信 详情请看man 7 unix
AF_INET IPv4 提供的网络通信 详情请看man 7 ip
AF_INET6 IPv6 提供的网络通信 详情请看man 7 ipv6
参数2:指定通信语义,可以由多个宏值使用位或连接
SOCK_STREAM:表示提供TCP协议的传输方式
SOCK_DGRAM:表示提供UDP协议的传输方式
SOCK_NONBLOCK:套接字设置非阻塞属性
参数3:如果参数2中仅仅指定一个协议,那么参数3可以填0,如果指定多个,则参数3需要指定特定的协议
TCP协议名称:IPPROTO_TCP
UDP协议名称:IPPROTO_UDP
返回值:成功返回创建的套接字文件描述符,失败返回 -1并置位错误码
三、TCP实现网络通信
1、原理
服务器端:
1)创建套接字1
2)给套接字1绑定服务器端端口号、ip地址
3)将套接字1的功能改为监听(套接字内部被改造,原本的收发缓冲区改为已连接、未连接队列),用于检测是否客服端连接(三次握手就发生在这一步)
4)阻塞等待连接,连接成功,创建套接字2,用于消息的收发
5)消息的发送与接收
6)关闭通信,可以由服务器端、客服端其中之一执行
客服端:
1)创建由于通信的套接字
2)绑定客服端端口号、ip地址
3)连接服务器端,连接成功进入未连接队列,马上从未连接队列向已连接队列转换,该过程非常迅速,但同时申请连接的数量过多(超过未连接队列大小)仍会阻塞
4)消息的发送与接收
5)关闭通信,可以由服务器端、客服端其中之一执行
2、TCP通信原理图
3、TCP相关函数
1)bind 绑定
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:位套接字分配名称
参数1:通过socket函数创建出来的套接字文件描述符
参数2:通用地址信息结构体,需要根据具体使用的地址族而定, struct sockaddr仅仅只是为了类型的强制转换,防止出现警告
跨主机间通信:man 7 ip
struct sockaddr_in {
sa_family_t sin_family; /* 表示通信域 */
in_port_t sin_port; /* 端口号的网络字节序 */
struct in_addr sin_addr; /* ip地址 */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* IP地址的网络字节序 */
};
同一主机间通信:man 7 uninx
struct sockaddr_un {
sa_family_t sun_family; /* 表示通信域:AF_UNIX */
char sun_path[108]; /* 套接字文件的地址 */
};
参数3:参数2的大小
返回值:成功返回0,失败返回-1并置位错误码
注意关于bind的两个错误:
1、 Cannot assign requested address:表示IP地址填写错误,检查IP是否有问题
2、Address already in use:表示地址信息正在占用,可以调用函数快速重用,也可以等一会
2)listen 监听
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:将套接字设置成被动监听状态,已接受客户端的连接请求
参数1:套接字文件描述符
参数2:容纳连接的队列的最大长度,一般填128
返回值:成功返回0,失败返回-1并置为错误码
3)accept 接收连接请求
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:用于阻塞接收客户端连接请求
参数1:服务器套接字文件描述符
参数2:用于接收对端地址信息结构体的指针
参数3:接收对端地址信息的长度
返回值:成功返回一个新的用于通信的套接字文件描述符,失败返回-1并置位错误码
4)recv 接收
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:从套接字中读取数据到buf中
参数1:用于通信的套接字文件描述符
参数2:接收数据后的容器地址
参数3:接收的数据的大小
参数4:是否阻塞接收
0:表示阻塞接收消息
MSG_DONTWAIT:表示非阻塞接收数据
返回值:
>0:表示成功读取的字符个数
=0:表示通信对端已经下线
=-1:表示出错,置位错误码
5)send 发送
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:向通信套接字文件描述符中写入数据
参数1:通信的套接字文件描述符
参数2:要发送数据的起始地址
参数3:要发送数据的大小
参数4:是否阻塞接收
0:表示阻塞接收消息
MSG_DONTWAIT:表示非阻塞接收数据
返回值:成功返回发送字符的个数,失败返回-1并置位错误码
6)connect 连接请求
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:将套接字文件描述符连接到addr指向的地址空间中
参数1:客户端套接字文件描述符
参数2:对端地址信息结构体
参数3:参数2的大小
返回值:成功返回0,失败返回-1并置位错误码
4、TCP服务器端代码实现
#include <myhead.h>
#define SER_PORT 6666
#define SER_IP "192.168.232.129"
int main(int argc, char const *argv[])
{
// 1、创建套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
// 参数1:ipv4的网络通信
// 参数2:TCP通信方式
// 参数3:默认使用一个协议
if (sfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success, sfd = %d\n", sfd); // 3
// 2、为套接字绑定ip地址和端口号
// 2.1 填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; // 通信域
sin.sin_port = htons(SER_PORT); // 端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址
// 2.2 绑定
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
// 3、将套接字设置为被动监听状态,用于接收
if (listen(sfd, 128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
// 4、阻塞等待客户端的连接请求
// 4.1 定义n变量用于e接收客服端的信息
struct sockaddr_in cin;
socklen_t addrlen = sizeof(cin);
// 4.2 接收连接
int newsfd = accept(sfd, (struct sockaddr *)&cin, &addrlen);
if (newsfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]:accept on\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
//5、数据收发
char buf[128] = "";
while (1)
{
// 从客户端套接字中接收数据
int res = recv(newsfd, buf, sizeof(buf),0);
if (res == -1)
{
perror("read error");
return -1;
}
else if (res == 0)
{
printf("客户端已下线\n");
close(newsfd); // 关闭客户端套接字
break;
}
// 接收数据
printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), buf);
// 对接收到的数据进行处理
strcat(buf, ":D");
// 将消息返回到客户端
if (send(newsfd, buf, strlen(buf),0) == -1)
{
perror("发送失败\n");
return -1;
}
printf("发送成功\n");
bzero(buf,sizeof(buf));//清空容器
}
return 0;
}
5、TCP客服端代码实现
#include <myhead.h>
#define SER_PORT 6666 // 与服务器保持一致
#define SER_IP "192.168.232.129" // 服务器ip地址
#define CLI_PORT 8888 // 客服端端口号
#define CLI_IP "192.168.232.129" // 客服端ip地址
int main(int argc, char const *argv[])
{
//1、 创建用于通信的套接字文件描述符
int cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1)
{
perror("socket error");
return -1;
}
printf("cfd = %d\n", cfd);
//2、 绑定IP地址和端口号
struct sockaddr_in cin;
cin.sin_family = AF_INET; // 通信域
cin.sin_port = htons(CLI_PORT); // 端口号
cin.sin_addr.s_addr = inet_addr(CLI_IP); // ip地址
//2.2、 绑定
if (bind(cfd, (struct sockaddr *)&cin, sizeof(cin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
//3、 连接服务器
//3.1、 填充服务器地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; //通信域
sin.sin_port = htons(SER_PORT);
sin.sin_addr.s_addr = inet_addr(SER_IP);
//3.2、连接服务器
if(connect(cfd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
{
perror("connect error");
return -1;
}
printf("连接服务器成功\n");
//4、数据收发
char buf[128] = "";
while (1)
{
printf("输入:");
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1] = 0;
//将数据发送到服务器
send(cfd,buf,strlen(buf),0);
printf("发送结束\n");
//接收服务器发送的数据
bzero(buf,sizeof(buf));//清空容器
recv(cfd,buf,sizeof(buf),0);
printf("收到服务器信息:%s\n",buf);
}
//5、关闭套接字
close(cfd);
return 0;
}
四、UDP实现网络通信
1、UDP网络通信模型
2、UDP相关函数 recvfrom sendto
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
//功能:从套接字文件描述符中读取数据,并将对端地址信息结构体接收
参数1:套接字文件描述符
参数2:要接收数据的起始地址
参数3:要接收的数据大小
参数4:是否阻塞,0表示阻塞,MSG_NOWAIT表示非阻塞
参数5:接收对端地址信息结构体
参数6:参数5的大小
返回值:成功返回读取的字节的大小,失败返回-1并置位错误码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
//功能:向套接字文件描述符中读取数据,写给指定的对端接收
参数1:套接字文件描述符
参数2:要发送数据的起始地址
参数3:要发送的数据大小
参数4:是否阻塞,0表示阻塞,MSG_NOWAIT表示非阻塞
参数5:接收对端地址信息结构体
参数6:参数5的大小
返回值:成功返回发送的字节的大小,失败返回-1并置位错误码
3、UDP服务器端代码实现
#include <myhead.h>
#define SER_PORT 9999 // 服务器端口号
#define SER_IP "192.168.232.129" // 服务器ip地址
int main(int argc, char const *argv[])
{
// 1、创建用于通信的套接字i文件描述符
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd == -1)
{
perror("scoket error");
return -1;
}
printf("sfd = %d\n", sfd); // 3
// 2、绑定ip地址和端口号
// 2.1 填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; // 通信域
sin.sin_port = htons(SER_PORT); // 端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址
// 2.2、 绑定
if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
// 3、数据收发
char buf[128] = "";
struct sockaddr_in cin; // 接收对端地址信息
socklen_t addrlen = sizeof(cin); // 接收地址长度
while (1)
{
// 清空容器
bzero(buf, sizeof(buf));
// 从套接字中读取数据
recvfrom(sfd, buf, sizeof(buf), 0,(struct sockaddr*)&cin,&addrlen);
printf("收到信息:%s\n", buf);
// 处理收到的信息
strcat(buf, ":(");
if (sendto(sfd, buf, sizeof(buf), 0,(struct sockaddr*)&cin,sizeof(cin)) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}
// 4、关闭文件描述符
close(sfd);
return 0;
}
4、UDP客服端端代码实现
#include <myhead.h>
#define SER_PORT 9999 // 与服务器保持一致
#define SER_IP "192.168.232.129" // 服务器ip地址
#define CLI_PORT 5555 // 客服端端口号
#define CLI_IP "192.168.232.129" // 客服端ip地址
int main(int argc, char const *argv[])
{
// 1、 创建用于通信的套接字文件描述符
int cfd = socket(AF_INET, SOCK_DGRAM, 0);
if (cfd == -1)
{
perror("socket error");
return -1;
}
printf("cfd = %d\n", cfd);
// 2、 绑定IP地址和端口号
struct sockaddr_in cin;
cin.sin_family = AF_INET; // 通信域
cin.sin_port = htons(CLI_PORT); // 端口号
cin.sin_addr.s_addr = inet_addr(CLI_IP); // ip地址
// 2.2、 绑定
if (bind(cfd, (struct sockaddr *)&cin, sizeof(cin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
// 3、数据收发
char buf[128] = "";
// 3.1 填充服务器地址信息结构体
struct sockaddr_in sin; // 接收对端地址信息
sin.sin_family = AF_INET; // 服务器的通信域
sin.sin_port = htons(SER_PORT); // 服务器的端口号
sin.sin_addr.s_addr = inet_addr(SER_IP); // 服务器的ip地址
while (1)
{
printf("输入:");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf) - 1] = 0;
sendto(cfd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin));
printf("发送成功\n");
bzero(buf, sizeof(buf));
recvfrom(cfd, buf, sizeof(buf), 0, NULL, NULL);
printf("收到服务器信息:%s\n", buf);
}
// 4、关闭套接字
close(cfd);
return 0;
}
五、TCP和UDP基础通信模型注意事项
1、无论时TCP还是UDP通信中,服务器必须绑定ip地址和端口号,以便于让客服端找到该服务器。对于客服端而言,ip地址和端口号可以不绑定,若不绑定,端口号由系统动态分配(49152-65535)
2、对于TCP通信而言,可以使用recv和send进行通信,也可以使用read、write进行通信,还可以使用sendto和recvfrom进行通信
3、对于UDP通信而言,如果当前端只是用于接收数据,不发送数据,可以使用recvfrom、recv、read进行接收;如果当前端接收数据后还要发送数据给对端,则需要使用recvfrom进行接收数据,以便接收对端地址信息结构体
4、UDP通信中,服务器端可以使用connect函数与指定的客服端建立一个唯一的通道,在解除这种连接前,其他客服端与服务器端间不能通信。可通过将与服务器端建立连接的那个客服端的地址消息结构体中的sin.family设置未 AF_UNSPEC, 后再次使用connect函数断开连接
UDP中通信使用connect连接的好处:
1)提高信息传输效率、完整度
例如:A和B同时向服务器发送消息,但是A发送的消息较大,需要较长的时间,发送过程中可能会出现时间片用完,服务器转而接收B的消息的情况,这会导致消息混乱。这时就可以先单独跟A建立连接,等所有数据传输结束后,再跟B通信
2)传输性能高
一般的UDP通信:获取对端地址信息 -->将信息加载到内核 -->数据收发--->获取对端地址信息 -->将信息加载到内核 --->数据收发 --->获取对端地址信息 -->将信息加载到内核 -->数据收发 -->......
会经历多次用户空间到内核空间的转换,该过程对于cpu而言是一个漫长的过程
UDP建立连接后:获取对端地址信息 ->将信息加载到内核 ->数据收发 ->数据收发 >数据收发 >数据收发完成>进行其他对端的信息处理.....
会有效的减少用户空间到内核空间的转换次数