204-socket 网络编程(1)

socket是套接字,通过套接字,进行网络数据的收和发
套接字就像网络中的“手机”

主机字节序列和网络字节序列

主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。
所以,在将数据发送到网络时规定整形数据使用大端字节序所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。
发送整数之前,要转成网络字节序列

Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换

#include <netinet/in.h>

uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序

uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序

uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序

uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序

套接字地址结构

通用 socket 地址结构
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

#include <bits/socket.h>

struct sockaddr//通用的
{
	sa_family_t sa_family;
	char sa_data[14];//占位的作用
};

sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示
在这里插入图片描述
不同的协议,地址不一样,宿舍是宿舍号,小区是哪一栋楼几零几。
专用 socket 地址结构
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于 IPV4 和 IPV6:

/*
sin_family: 地址族 AF_INET
sin_port: 端口号,需要用网络字节序表示
sin_addr: IPV4 地址结构:s_addr 以网络字节序表示 IPV4 地址
*/

struct in_addr
{
	u_int32_t s_addr;
};

struct sockaddr_in//IPV4 
{
	sa_family_t sin_family;
	u_int16_t sin_port;
	struct in_addr sin_addr;
};

struct in6_addr
{
	unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示 };

	struct sockaddr_in6
	{
		sa_family_t sin6_family; // 地址族:AF_INET6
		u_inet16_t sin6_port; // 端口号:用网络字节序表示
		u_int32_t sin6_flowinfo; // 流信息,应设置为 0
		struct in6_addr sin6_addr; // IPV6 地址结构体
		u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段
	};

IP 地址转换函数

通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); // 字符串表示的 IPV4 地址转化为网络字节序

char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符串表示

整数有4个字节。把每一个字节转换为整型。
在这里插入图片描述
每个字节的数肯定小于255
字符串表示:点和十进制

网络编程接口

#include <sys/types.h>
#include <sys/socket.h>

/*************************************************************
socket()创建套接字,成功返回套接字的文件描述符,失败返回-1
domain: 设置套接字的协议簇, AF_UNIX AF_INET AF_INET6
type: 设置套接字的服务类型 SOCK_STREAM  SOCK_DGRAM
protocol: 一般设置为 0, 表示使用默认协议
*************************************************************/
int socket(int domain, int type, int protocol);


/*************************************************************
bind()将 sockfd 与一个 socket 地址绑定,成功返回 0,失败返回-1
sockfd 是网络套接字描述符
addr 是地址结构
addrlen 是 socket 地址的长度
**************************************************************/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


/*************************************************************
listen()创建一个监听队列以存储待处理的客户连接,成功返回 0,失败返回-1
sockfd 是被监听的 socket 套接字
backlog 表示处于完全连接状态的 socket 的上限
**************************************************************/
int listen(int sockfd, int backlog);


/*************************************************************
accept()从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket,
该 socket 唯一地标识了被接收的这个连接,失败返回-1
sockfd 是执行过 listen 系统调用的监听 socket
addr 参数用来获取被接受连接的远端 socket 地址
addrlen 指定该 socket 地址的长度
*************************************************************/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);


/*************************************************************
connect()客户端需要通过此系统调用来主动与服务器建立连接,
成功返回 0,失败返回-1
sockfd 参数是由 socket()返回的一个 socket。
serv_addr 是服务器监听的 socket 地址
addrlen 则指定这个地址的长度
*************************************************************/
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);


/*************************************************************
close()关闭一个连接,实际上就是关闭该连接对应的 socket
*************************************************************/
int close(int sockfd);


/**************************************************************
TCP 数据读写:
recv()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小
send()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长
度
flags 参数为数据收发提供了额外的控制
**************************************************************/
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
ssize_t send(int sockfd, const void *buff, size_t len, int flags);


/**************************************************************
UDP 数据读写:
recvfrom()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大
小
src_addr 记录发送端的 socket 地址
addrlen 指定该地址的长度
sendto()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长
度
dest_addr 指定接收数据端的 socket 地址
addrlen 指定该地址的长度
**************************************************************/
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,
struct sockaddr* src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, void *buff, size_t len, int flags,
struct sockaddr* dest_addr, socklen_t addrlen);

TCP 编程流程

TCP 提供的是面向连接的、可靠的、字节流服务
TCP 的服务器端和客户端编程流程如下:
在这里插入图片描述
服务器端是被动等着别人去链接它,客户端是主动去链接服务器端。
socket()方法(手机) 是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM)。

服务端
bind()方法(手机号码) 是用来指定套接字使用的 IP 地址和端口。IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整形值,一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在Linux 上,1024 以内的端口号,只有 root 用户可以使用。

listen()方法(开机) 是用来创建监听队列。监听队列有两种,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。

accept()方法(接通电话) 处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。

rece()方法(收听内容)
send()方法(和你说话)
close()方法(挂断电话)

客户端
socket()方法(手机)
可以指定,但是这里不用指定,主机随机分配临时端口,但是客户端不需要指定。

connect()方法(拨打电话) 一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方法执行后,会进行三次握手, 建立连接。
在这里插入图片描述

send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。

recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果recv()返回值为 0, 说明对方已经关闭了 TCP 连接。

close()方法用来关闭 TCP 连接。此时,会进行四次挥手。

TCP 服务端代码 TcpServer.c 示例如下

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字,指定地址族,流式服务类型
	assert( sockfd != -1 );

	struct sockaddr_in saddr;//ipv4专用的 确定ip和端口,相当于手机号
	memset( &saddr, 0, sizeof(saddr) );//结构的内容全部清空,因为后面有占位有多余的部分
	saddr.sin_family = AF_INET;//地址族
	saddr.sin_port = htons(6000);//htons 将主机字节序转换为网络字节序
	saddr.sin_addr.s_addr = inet_addr( "127.0.0.1" ); //回环地址,把字符串转换为无符号整型

	int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));//命名指定
	assert( res != -1 );

	res = listen(sockfd, 5);//5是相当于处理窗口的个数,相当于开机
	assert( res != -1 );

	while( 1 )//服务器循环接收客户端的连接
	{
		struct sockaddr_in caddr;
		socklen_t len = sizeof(caddr);
		int c = accept( sockfd, (struct sockaddr*)&caddr, &len ); //阻塞,链接套接字
		if(c == -1)
		{
			perror("accept error ");
			continue;
		}

		printf("accept c = %d\n", c);
		char data[128] = {0};
		int n = recv(c, data, 127, 0);//阻塞,读到几个字节数,从c上接收

		printf("n = %d, buff = %s\n", n, data);

		send(c, "OK", 2, 0);

		close(c);
	}

	close(sockfd);

	exit(0);
}

在这里插入图片描述
在这里插入图片描述
这个IP地址(127.0.0.1)是测试用的,自测,能收能发数据,每个人都有,自己给自己发数据。
在这里插入图片描述
在这里插入图片描述
一个客户端对应一个C,客户端建立socket和服务端打招呼,服务端返回c。 c是链接套接字
在这里插入图片描述
在这里插入图片描述

TCP 客户端代码 TCPClient.c 示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	assert(sockfd != -1);

	struct sockaddr_in saddr;//建立套接字地址结构,指定服务端的,可以但是不需要确定自己的,自己的不需要,不影响,我们是打电话给服务器,自己手机号多少不重要!系统会分配临时端口,IP地址你是多少就多少
	memset(&saddr, 0, sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);//我们要去链接服务器的6000端口,不是自己的端口
	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
	//打电话不一定成功
	assert(res != -1);

	printf("please input: ");
	char buff[128] = {0};
	fgets(buff, 128, stdin);

	send(sockfd, buff, strlen(buff) - 1, 0);//用write也可以

	char data[128] = {0};
	int n = recv(sockfd, data, 127, 0);//用read也可以

	printf("%s\n", data);

	close(sockfd);

	exit(0);
}

在这里插入图片描述
在这里插入图片描述
运行截图如下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
n:端口号
a:地址
t:连接
p:pid

在这里插入图片描述在这里插入图片描述

复位报文段

在某些特殊条件下,TCP 连接的一端会向另一端发送携带 RST 标志的报文段,即复位报文段,已通知对方关闭连接或重新建立连接。这里介绍一下三种情况:
1)当户端端程序访问一个不存在的端口时,目标主机给它发送一个复位报文段。
2)异常终止连接。正常情况下,数据交换完成之后,一方给另一方发送 FIN 结束报文段。TCP 提供了异常终止一个连接的方法,即给对方发送一个复位报文段。一但发送了复位报文段,发送端所有排队等待发送的数据都将被丢弃。应用程序可以使用 socket 选项SO_LINGER 来设置发送复位报文段,以异常终止连接。
3) 处理半打开连接。例如 TCP 一端关闭了连接,由于网络故障对方没有收到结束报文,对方误以为连接仍然正常。处于这种状态的连接称为半打开连接。此时如果对端向连接写入数据,则会收到本端回复的复位报文段。

交互数据流与成块数据流

TCP 按照携带应用程序数据长度可以分为两种:交互数据和成块数据。交互数据仅包含很少的字节。使用交互数据的应用程序对实时性要求极高,比如 telnet、ssh 等。成块数据的长度则通过为 TCP 报文段允许的最大数据长度。使用成块数据的应用程序对传输效率要求高,比如 FTP。

带外数据

有些传输层协议具有带外(out of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事件。因此,带外数据比普通数据有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。
UDP 没有实现带外数据传输,TCP 也没有真正的带外数据。不过 TCP 利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供了一种传输紧急数据的方式。一般只有一个字节数据。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林林林ZEYU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值