目录
一.网络的五元组信息?
二.UDP的简单特性
?三.TCP的简单特性
?四.网络字节序
4.1主机字节序转换成网络字节序
4.2网络字节序转换成主机字节序
五.udpsocket编程流程
5.1编程流程示意图
?5.2编程流程
5.2.1创建套接字
5.2.2绑定地址信息
5.2.3UDP发送数据
5.3UDP的发送和接收缓冲区
六.tcpsocket编程
6.1编程流程示意图
6.3编程接口
6.3.1监听接口
6.3.2客户端连接接口
6.3.3服务端接收新连接
6.3.4TCP发送数据接口
6.3.5TCP接收数据接口
6.3.6单线程代码
6.3.7多线程代码?
一.网络的五元组信息?
一条网络一定包含的5部分信息:
- 源ip地址:表示该条消息来自那个机器
- 源端口:表示该条消息来自那个进程
- 目的ip地址:表示该条消息去往哪一个机器
- 目的端口:表示该条消息去往哪个进程
- 协议:双方网络数据采用的具体网络协议
二.UDP的简单特性
特性:无连接,不可靠,面向数据报
- 无连接:UDP客户端给服务端发送消息的时候,不需要和服务端先建立连接,直接发送(客户端也是不清楚服务端是否正在在线)
- 不可靠:UDP并不会保证数据是可靠有序到达对端
- 面向数据报:UDP数据不管是和应用层还是网络层传递,都是整条数据交付的
?三.TCP的简单特性
特性:面向连接,可靠传输,面向字节流
- 面向连接:双方在发送网络数据之前必须先建立连接,再进行发送
- 可靠传输:保证数据是可靠并且有序到达对端(有序是针对应用层而言的,TCP能够确保的是该条数据到达对端的应用层时,一定是有序到达的,到达传输层的时候有可能不是)
- 面向字节流:多次发送数据在网络传输当中没有明显的数据边界(对于没有明显间隔的数据橙称之为TCP粘包问题)
?四.网络字节序
- 小段字节序:低位存储在低地址,高位存储在高地址
- 大端字节序:第位存储在高地址,高位存储在低地址
- 网络字节序:采用的时大端字节序为大端
- 主机字节序:根据机器本身的字节序为小端。机器是大端,主机字节序为大端;机器是小端,主机字节序为小端。(网络设备就可以依据网络传输当中的ip和端口进行传输,要注意的是主机字节序换成网络字节序。)
4.1主机字节序转换成网络字节序
//端口
unit16_t htons(unit16_t hostsgort);
//ip
in_addr_t inet_addr(const char* cp);
- 将点分十进制的ip字符串转换成uint32_t的整数
- 将uint32_t的证书从主机字节序转换成网络字节序
4.2网络字节序转换成主机字节序
//端口
uint16_t ntohs(uint16_t netshort);
//ip
char* inet_ntoa(struct in_addr in);
- 将ip地址从网络字节序转换成主机字节序
- 将uint32_t的整数转换成为点分十进制的字符串
五.udpsocket编程流程
5.1编程流程示意图
?5.2编程流程
5.2.1创建套接字
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
- domain:地址域(指定网络用什么协议)
AF_INET | 使用ipv4版本的ip协议 | AF_INET6 | 使用ipv6版本的ip协议 | AF_UNIX | 本地域套接字(适用于一台机器两个进程,进程间通信) |
- type:创建套接字的类型
UDP : SOCK_DGRAM | 用户数据包套接字 | TCP : SOCK_STREAM | 流式套接字 |
- protocol:表示使用的协议?
0(采用套接字类型对应的默认协议) | SOCK_DGRAM | 默认协议就是UDP | SOCK_STREAM | 默认的协议就是TCP | 宏定义 | IPPROTO_UDP | UDP协议 | IPPROTO_TCP | TCP协议 |
返回值:还会套接字描述符,本质上就是文件描述符(也就是我们用户进程和网络协议栈进行交互的网络文件描述符,也就是说,发送数据的时候,要使用什么样的信息有套接字描述符进行决定)。
5.2.2绑定地址信息
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
参数:
- sockfd:套接字描述符,socket函数的返回值
- addr:地址信息结构
- struct sockaddr类型:通用的结构体类型,具体采用的是各个协议自己的数据(为了区分bind的调者传递进来的结构体是什么类型,他在这里做出了规定,如下图所示)?
- ?addrlen:告诉了内核当前这个地址信息结构体的长度是多少个,防止内核在解析结构体时地址越界。
代码:
5.2.3UDP发送数据
ssize_t sendto(int sockfd,const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
?参数:
- sockfd:套接字描述符
- buf:待要发送的数据
- len:发送数据的长度
- flags:0表示阻塞接收
- dest_addr:当前消息的目的地址信息结构(消息发送到哪里去)
- addr_len:地址信息结构长度
返回值:
ssize_t recvfrom(int sockfd, void *buf,size_t len, int flags,
struct sockaddr *str_sddr, socklen_t addrlen);
- sockfd:套接字描述符
- buf:将接收的数据存放到buf中
- len:最大接收能力
- flags:0表示阻塞接受
- str_addr:表示数据从哪一个地址信息结构来的(消息从那个ip和端口来的)
- addrlen:地址信息长度?(他是一个输入输出型参数)
代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
//socket编程需要包含的头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sockfd < 0){
perror("socket");
return 0;
}
printf("socket : %d\n", sockfd);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(28989);
addr.sin_addr.s_addr = inet_addr("172.17.0.3");
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0){
perror("bind");
return 0;
}
while(1){
char buf[1024] = {0};
//拿来对端地址信息,并且进行准备
struct sockaddr_in peer_addr;
socklen_t len = sizeof(peer_addr);
//size_t是无符号的,ssize_t时有符号的,换句话说,它里面是为了通配不同的操作系统位数
ssize_t recv_size = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer_addr, &len);
if(recv_size < 0){//接受失败
continue;
}
printf("recv msg \"%s\" from %s:%d\n", buf, inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
memset(buf, '\0', sizeof(buf));
//格式化字符串sprintf(),但是他不安全
//可以使用snprintf(char *str, size_t size, const char *format,...);
sprintf(buf, "welcome client %s:%d", inet_ntoa(peer_addr.sin_addr), ntohs(peer_addr.sin_port));
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&peer_addr, sizeof(peer_addr));
}
//关闭套接字
close(sockfd);
return 0;
}
5.3UDP的发送和接收缓冲区
udp的发送缓冲区只干了一件事情,拿到数据,打上udp包头就直接递交给网络层。我们在写代码时,交互最多的是传输层,而传输层与网络层在交互时我们不会参与,传输层数据链路层之间的数据传递我们也不会参与,都是网络协议栈上的net代码。
ps:在socket编程当中,绑定的是本地网卡的ip地址(不需要关心是公网ip还是私网ip),在互联网连接云服务器的时候,需要使用公网ip,云服务器厂商会帮助进行转换的。
六.tcpsocket编程
6.1编程流程示意图
6.2TCP发送/接收缓冲区
- 客户端连接服务端,在我们服务端的接收缓冲区当中,一发起连接这个套接字中就有一个连接请求,在内核当中三次握手,握手完毕后请求就已经建立了。
- 建立之后,此时我们调用accept(),在当前的服务端当中就会新创建出来一个套接字,这个套接字(就是调用接收连接函数获取到的新连接的套接字)就是专注为这个客户端进行服务的
- 最上面的套接字还是在的,这个套接字只有一个作用:接收连接,底下的套接字才真正与客户端进行网络通信。
6.3编程接口
第一步是创建套接字(socket函数),下来是绑定地址信息(bind函数),然后是监听接口。
6.3.1监听接口
int listen(int sockfd,int backlog);
- sockfd:侦听套接字,socket函数的返回值创建的
- backlog:指定内核当中已完成连接队列的大小(已完成连接队列的大小决定了服务端的并发连接数。指的是在同一时刻服务端能够处理的连接数量上限(并不是服务端能够接受连接的)) **重点**内核当中有连个队列一个叫做已完成连接队列,一个是未完成连接队列。1.客户端调用连接接口之后他想去连接服务端一定会经历三次握手的过程,而当它在服务端经历三次握手的时候,针对当前的链接,就会被放在未完成连接队列;2.若三次握手完毕,连接建立了,那么当前的连接就会从未完成连接队列移动到已完成连接队列里。此时这些过程全部是在内核当中的过程,3.连接建立完成之后,此时的链接是在已完成链接队列当中,当我们在程序当中调用接收函数(accept());他才会从已完成连接队列当中将连接拿出来,所以说accept才会给我们返回了一个新连接的套接字
引申问题:TCP服务端最大接收多少连接?
取决于操作系统对进程当中打开文件描述符的限制。(这个限制我们可以在ulimit -a中进行查找,而且还可以通过ulimit进行更改)
6.3.2客户端连接接口
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
- sockfd:套接字描述符
- addr:要连接的服务端的地址信息结构(而在地址信息结构中存在服务端的ip地址和服务端的接口)
- addrlen:地址信息长度
返回值:返回值为0时成功;返回值为-1为失败
此时连接完毕,现在缺少接收连接。
6.3.3服务端接收新连接
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
参数:
- ?sockfd:侦听套接字(socket函数的返回值)
- addr:客户端的地址信息结构(出参:由accept函数返回客户端的地址信息结构)
- addrlen:客户端的地址信息结构长度(出参:有accept函数返回客户端的地址信息结构长度)
返回值:返回新连接的套接字描述符(>=0:成功,<0:失败)
当调用accept函数接收新连接的时候,如果已经完成连接队列当中没有新的连接,则accept函数就会阻塞。
接受结束后,就要进入通信阶段。
6.3.4TCP发送数据接口
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
6.3.5TCP接收数据接口
ssize_t recv(int socked, void *buf, size_t len,int flags);
- sockfd:套接字描述符的返回值.如果是客户端调用这个函数,那么此时就是socket函数的返回值;如果是服务端调用,那么是accept函数的返回值(切记不是侦听套接字)。
- buf:将从TCP接收缓冲区当中接受的数据保存在部分当中
- len:buf最大的接受能力
- flags:当flags为0时,阻塞接受
返回值:
<0 | 函数调用出错 | ==0 | 对端关闭连接 | >0 | 接收到的字节数量 |
如果接收缓冲区没有数据,调用recv函数去接收数据的时候,则会recv函数也会阻塞。如果将新连接套接字设成为非阻塞属性,则调用recv函数的时候,即使接收缓冲区当中没有数据,该函数也会返回。返回值是表现的不是正常返回,而是出错返回。
关闭套接字文件描述符需要的接口:close(int sockfd);
问题解决:
- 服务端的侦听端口(如果UDP已经绑定了某个端口,TCP是否可以绑定):一个端口可以同时被UDP程序和TCP程序所绑定,原因是在网络层已经能够区分一个网络数据是传递给传输层的UDP协议和TCP协议。
- telnet测试TCP端口是否开放(是否在监听),引申含义是服务端是否在正常工作:给TCP的服务端发送建立连接的请求,也可以模拟三次握手的过程,建立TCP连接。
- backlog(测试已完成连接队列):能够和服务端建立连接数量 = backlog + 1
- 正常就收新连接,发送/连接数据
6.3.6单线程代码
//server
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listen_sock < 0)
{
perror("socket");
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(28989);
//0.0.0.0 : 本地所有的网卡地址
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return 0;
}
ret = listen(listen_sock, 1);
if(ret < 0)
{
perror("listen");
return 0;
}
struct sockaddr_in cli_addr;
socklen_t cli_addrlen = sizeof(cli_addr);
//调用accept函数
int newsockfd = accept(listen_sock, (struct sockaddr*)&cli_addr, &cli_addrlen);
if(newsockfd < 0)
{//接受失败
perror("accept");
return 0;
}
printf("accept new connect from client %s:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
while(1)
{
//对服务端进行接收
char buf[1024] = {0};
ssize_t recv_size = recv(newsockfd, buf, sizeof(buf) - 1, 0);
if(recv_size < 0)
{
perror("recv");
continue;
}
else if(recv_size == 0)
{
printf("peer close connect\n");
//说明对端关闭掉了
close(newsockfd);
continue;
}
printf("%s\n", buf);
memset(buf, '\0', sizeof(buf));
strcpy(buf, "i am server!!!");
send(newsockfd, buf, strlen(buf), 0);
}
close(listen_sock);
return 0;
}
//client
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(28989);
//0.0.0.0 : 本地所有的网卡地址
addr.sin_addr.s_addr = inet_addr("42.192.83.143");
int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("connect");
return 0;
}
while(1)
{
char buf[1024] = "i am client1111222";
send(sockfd, buf, strlen(buf), 0);
memset(buf, '\0', sizeof(buf));
//接收
ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
if(recv_size < 0)
{
perror("recv");
continue;
}
else if(recv_size == 0)
{
printf("peer close connect\n");
close(sockfd);
continue;
}
printf("%s\n", buf);
sleep(1);
}
close(sockfd);
return 0;
}
6.3.7多线程代码?
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
struct ThreadInfo
{
int newsockfd_;
};
//线程入口函数
void* TcpThreadStart(void* arg)
{
pthread_detach(pthread_self());
struct ThreadInfo* ti = (struct ThreadInfo*)arg;
int newsockfd = ti->newsockfd_;
while(1)
{
//接收
char buf[1024] = {0};
ssize_t recv_size = recv(newsockfd, buf, sizeof(buf) - 1, 0);
if(recv_size < 0)
{
perror("recv");
continue;
}
else if(recv_size == 0)
{
printf("peer close connect\n");
close(newsockfd);
break;
}
printf("%s\n", buf);
memset(buf, '\0', sizeof(buf));
strcpy(buf, "i am server!!!");
send(newsockfd, buf, strlen(buf), 0);
}
delete ti;
return NULL;
}
int main()
{
int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listen_sock < 0)
{
perror("socket");
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(28989);
//0.0.0.0 : 本地所有的网卡地址
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return 0;
}
ret = listen(listen_sock, 1);
if(ret < 0)
{
perror("listen");
return 0;
}
while(1)
{
struct sockaddr_in cli_addr;
socklen_t cli_addrlen = sizeof(cli_addr);
int newsockfd = accept(listen_sock, (struct sockaddr*)&cli_addr, &cli_addrlen);
if(newsockfd < 0)
{
perror("accept");
return 0;
}
printf("accept new connect from client %s:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
struct ThreadInfo* ti = new ThreadInfo;
ti->newsockfd_ = newsockfd;
//创建线程
pthread_t tid;//给定一个临时变量
ret = pthread_create(&tid, NULL, TcpThreadStart, (void*)ti);
if(ret < 0)
{//创建线程失败
close(newsockfd);
delete ti;//线程创建失败,delete掉
continue;
}
}
close(listen_sock);
return 0;
}
|