Linux网络编程socket通信1

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,报式代表协议UDP0 表示使用默认协议。
返回值:
	成功:返回指向新创建的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通信流程

serverclient
1、socket 创建socket1、socket 创建socket
2、bind 绑定服务器ip和port2、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 标志位。 ----- 连接全部关闭

  • 滑动窗口:

​ 发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值