目录
TCP_Socket 通信1
Socket网络套接字
- 网络套接字: socket
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)
在通信过程中, 套接字一定是成对出现的。
网络字节序
TCP/IP中规定了,网络数据流应采用大端字节序,即低地址高字节,称为网络字节序。
而我们的电脑主机一般是采用小端字节序的,也就是低地址低字节,称为主机字节序。
- 网络字节序和主机字节序的转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // int整型(32bit)数据 主机字节序转为网络字节序 eg:ip地址
// 192.168.101.1 他是string类型的,转化为int类型,使用atoi()函数
uint16_t htons(uint16_t hostshort);// short数据 主机字节序转为网络字节序
uint32_t ntohl(uint32_t netlong);// int整型(32bit)数据 网络字节序转为主机字节序
uint16_t ntohs(uint16_t netshort);// short数据 网络字节序转为主机字节序
// h表示host,n表示network,l表示32位长整数,s表示16位短整数
// 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
// 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
#include <stdlib.h>
int atoi(const char *nptr); // 将字符串转化为整型 a-->char i-->int
long atol(const char *nptr);// 将字符串转化为long int类型 l-->long int
long long atoll(const char *nptr);// 将字符串转化为长长整型 ll-->long long(64bit)
IP地址转换函数
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
// p-->ip n-->network 将ip地址字符串转换为网络字节序
// 参数af--> AF_INET or AF_INET6
// 参数src--> 传入的ip地址字符串,点分十进制
// 参数dst--> 传出网络字节序ip地址
// return:成功返回1 src指向不是一个有效地址返回0 失败返回-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// 将网络字节序转换为ip地址字符串
// 参数af--> AF_INET or AF_INET6
// 参数src--> 传入的网络字节序ip地址
// 参数dst--> 传出参数ip地址字符串
// 参数size--> dst的大小
// return:成功返回dst 失败返回NULL
sockaddr结构体
strcut sockaddr
很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr
退化成了(void *)
的作用,传递一个地址给函数,至于这个函数是sockaddr_in
还是sockaddr_in6
,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
struct sockaddr { // 旧的结构体 一般不使用
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
//使用man 7 ip查看
struct sockaddr_in { // ipv4使用的结构体
__kernel_sa_family_t sin_family; /* Address family */ //地址结构类型
__be16 sin_port; /* Port number */ //端口号
struct in_addr sin_addr; /* Internet address */ //IP地址
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)]; // 8字节的填充
};
struct in_addr { /* Internet address. */
__be32 s_addr;
};
struct sockaddr_in6 { // ipv6使用的结构体
unsigned short int sin6_family; /* AF_INET6 */
__be16 sin6_port; /* Transport layer port # */ //端口号
__be32 sin6_flowinfo; /* IPv6 flow information */ // flow label 32bit
struct in6_addr sin6_addr; /* IPv6 address */ // ip地址 128bit
__u32 sin6_scope_id; /* scope id (new in RFC2553) */ // scope ID
};
struct in6_addr { // ipv6 ip地址存放结构体
union {
__u8 u6_addr8[16];
__be16 u6_addr16[8];
__be32 u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};
#define UNIX_PATH_MAX 108
struct sockaddr_un { // 本地套接字使用结构体
__kernel_sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
- 结构体的使用
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8234);
int dst = 0;
inet_pton(AF_INET,"192.168.22.123", (void*)&dst);
// 上面的相当于int dst = htonl(atoi("192.168.22.123"));
addr.sin_addr.s_addr = dst;
// addr.sin_addr.s_addr = htonl(atoi("192.168.22.123"));
// addr.sin_addr.s_addr = htonl(INADDR_ANY);// 自动取系统中有效的任意一个IP地址
bind(fd,(struct sockaddr*)&addr,sizeof(addr));
网络套接字函数
- TCP服务端描述:
socket
函数参生一个套接字文件描述符,bind
函数绑定ip和port,listen
函数设置监听上限,accept
函数阻塞监听客户端连接,监听到返回一个新的套接字用于通信,之前socket
产生的套接字继续回去监听,当有一个客户端连接后,进行通信,read读,write写,当read读取到0的时候close。
- TCP客户端描述:
socket函数参生一个套接字文件描述符 ,connect函数和指定连接的ip地址和port进行绑定,进行通信,read和write。
socket通信建立一共有3个套接字
socket函数
#include <sys/socket.h>
// 参数socket套接字文件描述符
int socket(int domain, int type, int protocol);
domain: // ip地址协议使用的是什么
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type: // 数据传输协议使用方式
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。// 流式
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。// 报式
SOCK_SEQPACKET 该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW socket 类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol: // 前面使用的代表协议是什么 比如流式代表协议TCP,报式代表协议UDP
传0 表示使用默认协议。
返回值:
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1){
perror(fd);
exit(1);
}
bind函数
#include <sys/socket.h>
// 给套接字文件描述符绑定ip地址和端口号(地址结构)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
socket文件描述符
addr:
构造出IP地址加端口号
addrlen:
sizeof(addr)长度
返回值:
成功返回0,失败返回-1, 设置errno
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9527);
//int dst = 0;
//inet_pton(AF_INET,"192.168.22.123", (void*)&dst);
//addr.sin_addr.s_addr = dst;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 自动取系统中有效的任意一个IP地址
bind(fd,(struct sockaddr*)&addr,sizeof(addr));
listen函数
#include <sys/socket.h>
//声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态
//如果接收到更多的连接请求就忽略
//设置三次握手的最大数量
int listen(int sockfd, int backlog);
sockfd:
socket文件描述符
backlog:// 上限数值。最大值 128.
排队建立3次握手队列和刚刚建立3次握手队列的链接数和
return:成功返回0,失败返回-1
int ret = listen(listenfd, 20);
if(ret == -1){
perror(ret);
exit(1);
}
accept函数
#include <sys/socket.h>
// 阻塞等待客户端建立连接,成功的话,
// 返回一个与客户端成功连接的socket文件描述符。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
socket文件描述符
addr:
传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
传入传出参数(值-结果),传入sizeof(addr)大小,
函数返回时返回真正接收到客户端地址结构体的大小
返回值:
成功返回一个新的socket文件描述符,
用于和客户端通信,失败返回-1,设置errno
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE); // 用accept返回的文件描述符通信
//......
close(connfd);
// 不关闭之前socket函数产生的文件描述符,
//继续用于accept阻塞监听
}
connect函数
#include <sys/socket.h>
// 指定连接服务器的ip地址和port进行绑定,发起连接并阻塞等待服务器应答,
// 完成三次握手的过程
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功返回0,失败返回-1,设置errno
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9527);
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
connect(fd,(struct sockaddr*)&addr,sizeof(addr));
// 如果不使用bind绑定客户端地址结构, 采用"隐式绑定". 客户端的ip地址和端口号系统自动分配
CS模型的TCP通信流程
server | client |
---|---|
1、socket 创建socket | 1、socket 创建socket |
2、bind 绑定服务器ip和port | 2、connect 与服务器建立好连接 |
3、listen 设置服务器监听上限 | 3、write 写数据给服务器 |
4、accept 阻塞监听客户端连接 | 4、read 读取服务器下发的结果 |
5、read 读socket获取客户端数据 | 5、close 关闭通信 |
6、处理客户端上传数据 | |
7、write 下发结果给客户端 | |
8、close 关闭通信 |
sever端代码实现
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#define SER_PORT 9527
void sys_error(const char* string){
perror(string);
exit(1);
}
int main(int argc, char* argv[]){
int lfd = 0,cfd = 0; // 服务器连接fd和与客户端通信fd
struct sockaddr_in saddr,caddr;
socklen_t clen;
char client_IP[1024], buf[BUFSIZ];
bzero(&saddr, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(SER_PORT);
saddr.sin_addr.s_addr = htonl(INADDR_ANY); // 系统随机使用可用的ip地址
lfd = socket(AF_INET, SOCK_STREAM, 0); // 创建socket
if(lfd == -1){
sys_error("socket error");
}
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr)); // 绑定服务器地址结构
if(ret == -1){
sys_error("bind error");
}
ret = listen(lfd,128); // 设置监听上限
if (ret == -1){
sys_error("listen error");
}
clen = sizeof(caddr);
cfd = accept(lfd,(struct sockaddr* )&caddr, &clen);
if(cfd == -1){
sys_error("accept error");
}
// 使用inet_ntop将网络字节序转为ip地址字符串
printf("client ip:%s port:%d\n",
inet_ntop(AF_INET,& caddr.sin_addr.s_addr,
client_IP,sizeof(client_IP)),
ntohs(caddr.sin_port));// 根据accept传出参数,获取客户端 ip 和 port
int i = 0;
while(1){
ret = read(cfd,buf,sizeof(buf));
write(STDOUT_FILENO, buf, ret);// 写到屏幕查看
for (i = 0; i < ret; i++) // 转为大写的
{
buf[i] = toupper(buf[i]);
}
write(cfd,buf,ret); // 写给客户端
}
close(lfd);
close(cfd);
return 0;
}
client端代码实现
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#define SER_PORT 9527
#define SER_IP "127.0.0.1"
void sys_error(const char *string)
{
perror(string);
exit(1);
}
int main(int argc, char* argv[]){
int cfd;
char buff[BUFSIZ];
struct sockaddr_in addr; // 服务器端的ip地址
bzero(&addr, sizeof(addr)); // 清空结构addr
cfd = socket(AF_INET, SOCK_STREAM, 0);
if(cfd == -1){
sys_error("socket error");
}
addr.sin_family = AF_INET;
addr.sin_port = htons(SER_PORT);
inet_pton(AF_INET,SER_IP,& addr.sin_addr.s_addr);
// inet_pton(AF_INET,SER_IP,& addr.sin_addr);
// 这样写也是可以的
int ret1 = connect(cfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret1 == -1){
sys_error("connect error");
}
while(1){
int ret = read(STDIN_FILENO, buff, sizeof(buff));
// 从屏幕中读取数据
write(cfd, buff, ret); // 写到服务器中
ret = read(cfd, buff, sizeof(buff)); // 读取结果
write(STDOUT_FILENO, buff, ret);
sleep(1);
}
close(cfd);
return 0;
}
TCP通信时序
三次握手建立连接
- SYN标志位:请求建立连接标志位, SYN 1000(0) <mss 1460> 表示第1000(32bit序号)包发送数据0,数据包数据上限是1460,用于建立连接。
- ACK应答标志位,服务端同意建立连接就会应答。ACK 1001, mss<1024> 这里的1001(32bit确认序号)表示前面的1000个数据,我都接收到了。SYN 8000(0)表示和客户端建立连接,发送第8000包数据为0。客户端回应后,那么三次握手建立连接。
- 三次握手发生在内核空间,就是accept和connect函数都成功执行并返回正确了。
数据通信
- 1001(20),ACK 8001 发送数据包20个数据。8001(10) ,ACK 1021 应答前面的数据都收到了,服务器并发送数据包10个数据。
- 8011(10),表示服务器发送的10个数据收到了。
- 按照上面的一回一答通信很慢,其实TCP可以连续的发送,最后一起做应答,使用滑动窗口。
四次挥手断开连接
-
FIN,1021(0),ACK 8011:FIN表示finish,完成的意思,用于断开连接。客户端发送断开请求。
-
ACK 1022,服务器应答客户端断开请求,发送应答,此时处于半关闭的状态。此时服务器还可以下发数据,但是客户端不能上传数据。
-
FIN,8011(0),ACK 1022 ,请求和客户端关闭通信。
-
ACK 8012 客户端应答,此时全部关闭。
滑动窗口
对于UDP来说,如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。TCP协议通过“滑动窗口(Sliding Window)”机制解决这一问题。
-
1、发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
-
2、发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
-
3、接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
-
4、接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
-
5、发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
-
6、接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
-
7、接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
-
8、接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
-
9、接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。
-
上图在接收端用小方块表示1K数据,实心的小方块表示已接收到的数据,虚线框表示接收缓冲区,因此套在虚线框中的空心小方块表示窗口大小,从图中可以看出,随着应用程序提走数据,虚线框是向右滑动的,因此称为滑动窗口。
总结
- 三次握手:
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。
被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。
主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
- 四次挥手:
主动关闭连接请求端, 发送 FIN 标志位。
被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。
被动关闭连接请求端, 发送 FIN 标志位。
主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
- 滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。