专业编程基础技术教程

网站首页 > 基础教程 正文

Linux网络编程——详解SOCKET

ccvgpt 2024-11-21 11:07:52 基础教程 1 ℃

一、预备知识

大端模式、小端模式

  • 大端字节序(Big Endian):最高有效位存于最低内存地址处,最低有效位存于最高内存处;
  • 小端字节序(Little Endian):最高有效位存于最高内存地址,最低有效位存于最低内存处。

网络字节序

  • 我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理
  • 网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
  • TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,因此网络数据流应采用大端字节序,即低地址高字节

可以调用以下库函数做网络字节序和主机字节序的转换

#include <arpa/inet.h>

//这些函数调用成功后返回处理后的值,调用失败则返回-1
uint32_t htonl(uint32_t hostlong);	//主机字节顺序转换为网络字节顺序 对无符号长型进行操作4bytes
uint16_t htons(uint16_t hostshort); //主机字节顺序转换为网络字节顺序 对无符号短型进行操作2bytes  

uint32_t ntohl(uint32_t netlong);   //网络字节顺序转换为主机字节顺序 对无符号长型进行操作4bytes
uint16_t ntohs(uint16_t netshort);  //网络字节顺序转换为主机字节顺序 对无符号短型进行操作2bytes

IP地址转换函数

  • Linux提供了用于将点分十进制表示的IP地址与二进制表示的IP地址相互转换的函数族

早期

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

//数字加点类型 转换成 将32位的IP    IP地址存放在参数straddr中,返回结果存放在addrptr中 
int inet_aton(const char *straddr, struct in_addr *addrptr);

//将32位的IP 转换成 数字加点类型
char *inet_ntoa(struct in_addr straddr);

//数字加点类型 转换成 将32位的IP
in_addr_t inet_addr(const char *cp);

/*只能处理IPv4的ip地址
不可重入函数
注意参数是struct in_addr*/

现在

Linux网络编程——详解SOCKET

#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

/*支持IPv4和IPv6
可重入函数*/

Linuxc/c++服务器开发高阶视频,电子书学习资料后台私信【架构】获取

sockaddr数据结构

  • Linux中定义了一种通用的套接字结构类型strcut sockaddr,以供不同的协议调用
  • strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结
    构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个地址类型是sockaddr_in还是sockaddr_in6(文章没有列举出),由地址族确定,然后函数内部再强制类型转化为所需的地址类型
#include <sys/socket.h>

struct sockaddr
 {
	unsigned short sa_family; /* address族, AF_xxx */
	char sa_data[14]; 	      /* 14 bytes的协议地址 */
};

参数sa_family可选择如下

  • AF_INET IPv4协议
  • AF_INET6 IPv6协议
  • AF_LOCAL UNIX协议
  • AF_LINK 链路地址协议
  • AF_KEY 密钥套接字

除了sockaddr以外,Linux中还定义了另外一种结构类型sockaddr_in,它和sockaddr等效且可以互相转换(需要显式转换),通常在涉及TCP/IP的编程协议中使用

#include <netinet/in.h>

struct sockaddr_in
 {
	short int sin_family; 			/* Internet地址族 */
	unsigned short int sin_port;    /* 端口号 */
	struct in_addr sin_addr; 		/* Internet地址 */
	unsigned char sin_zero[8]; 		/* 添0(和struct sockaddr一样大小)*/
};

//其中in_addr由于历史设计原因导致结构体多余
struct in_addr
{
__be32 s_addr;//32位IPv4地址,网络字节序
};

网络设计模式

c/s 客户端/服务器

  • 需要开发客户端服务器,采用自定义协议
  • 必须先下载客户端,数据提前缓冲好
  • 需要考虑安全问题
  • 开发工作量大

b/s web/服务器

  • 不需要安装软件,点击浏览器就可以看到
  • 工作量小,客户端基本浏览器方式
  • 缺点:必须遵循http协议,动态加载数据

二、SOCKET

概述

  • linux中的网络编程通过socket接口实现。socket既是一种特殊的IO,它也是一种文件描述符

socket可以简单理解成为一个插座和插排,那么如何匹配?


就是通过IP+端口号进行匹配,匹配之后可以通过socket进行数据的发送和接收(socket本质是文件描述符fd)


具体的流程如下

socket创建

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

int socket(int domain, int type, int protocol);
  • domain:
    AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
    AF_INET6 与上面类似,不过是采用IPv6的地址
    AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
  • type:
    (1)SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
    (2)SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
    (3)SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的
    接受才能进行读取。
    (4)SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使
    用该协议)
    (5)SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数
    据包的顺序
  • protocol:
    0 默认协议
  • 返回值
    成功返回一个新的文件描述符(也叫监听套接字),失败返回-1

bind绑定

  • 在创建了套接字之后需要IP和端口号和套接字绑定在一起( IP地址:在网络环境中,唯一标识一台主机,端口号:在主机中唯一标识一个进程)
  • 前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd
    socket文件描述符
  • addr:
    构造出IP地址加端口号
  • addrlen:
    sizeof(addr)长度
  • 返回值
    成功返回0,失败返回-1, 设置errno

例如

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));//清0结构体
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8000);

listen

  • 创建了套接字之后通常需要等待客户端的连接,此时可以使用listen函数将该套接字转换为倾听套接字。
  • 可以指定同时连接的最大客户端数量
  • 若达到数量上限,新客户端等待其它已链接的客户端链接结束
#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);
  • sockfd:
    socket文件描述符
  • backlog:
    排队建立3次握手队列和刚刚建立3次握手队列的连接数和
  • 返回值
    成功返回0,失败返回-1

accept

  • 当服务器倾听到一个连接之后,可以使用函数accept从倾听套接字的完成连接队列中接收一个连接,如果这个完成连接队列为空,则会使得这个进程进入睡眠状态
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockdf:
    socket文件描述符
  • addr:
    传出参数,返回链接客户端地址信息,含IP地址和端口号
  • addrlen:
    传入传出参数,传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
  • 返回值
    成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

connect客户端连接函数

  • 客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址
#include <sys/types.h> 
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockdf:
    socket文件描述符
  • addr:
    传入参数,指定服务器端地址信息,含IP地址和端口号
  • addrlen:
    传入参数,传入sizeof(addr)大小
  • 返回值
    成功返回0,失败返回-1,设置errno

读写函数

<unistd.h>

int read(int fd, char *buf, int len);
int write(int fd, char *buf, int len);
  • fd
    套接字描述符;
  • buf
    指定数据缓冲区;
  • len
    指定接收或发送的数据量大小(以字节为单位)。
  • 返回值
    返回读/写成功的数据量大小,失败则返回-1。

关闭函数

<unistd.h>

int close(int fd);
  • fd
    套接字描述符;

写一个服务器例子和客户端例子

服务器

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

#define Port 6666 //端口号
#define MAXCLIENT 10 //最大客户端数量

int main(int argc, char argv[])
{
	int socket_fd, client_fd;
	int ret;
	int addr_size;
	struct sockaddr_in server_addr;   
	struct sockaddr_in client_addr; 
	
	int read_size;
	char buffer[1024]; 
	
	//创建socket
	socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if( socket_fd == -1)
	{
		printf("socket error\n");
		exit(1);
	}
	
	//绑定bind
	bzero(&server_addr, sizeof(struct sockaddr_in));//清空数据
	
	server_addr.sin_family = AF_INET;//IPv4
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//将主机IP转换为网络IP
	server_addr.sin_port = htons(Port);//将主机端口转换为网络Port	
	
	ret = bind(socket_fd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr));
	if(ret == -1)
	{
		printf("bind error\n");
		exit(1);
	}
	
	//监听
	ret = listen(socket_fd, MAXCLIENT);
	if(ret == -1)
	{
		printf("listen error\n");
		exit(1);
	}
	
	while(1)
	{
		//accept
		addr_size = sizeof(struct sockaddr_in);
		client_fd = accept(socket_fd, (struct sockaddr *)(&client_addr), &addr_size);
		if(client_fd == -1)
		{
			printf("accept error\n");
			exit(1);
		}
		//打印客户端IP   将网络地址转换成 .字符串 
		printf("Server get connection from %s\n",inet_ntoa(client_addr.sin_addr));
			
		if((read_size = read(client_fd, buffer, 1024)) == -1)    
		{     
			printf("Read Error\n");     
			exit(1);    
		} 
  	     
		buffer[read_size]='\0';   
		printf("Server received %s\n",buffer); 
			
		close(client_fd);    /* 循环下一个 */   		
	}
	
	close(socket_fd);   	
	return 0;
}

客户端

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

#define Port 6666

int main(int argc, char argv[])
{
	int socket_fd;
	int ret;
	char buff[1024];
	struct sockaddr_in server_addr;
	
	char* str_IP = "172.21.252.7";
	
	//创建客户端socket
	socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if( socket_fd == -1)
	{
		printf("socket error\n");
		exit(1);
	}
	
	//连接connect
	bzero(&server_addr, sizeof(struct sockaddr_in));//清空数据
	
	server_addr.sin_family = AF_INET;//IPv4
	server_addr.sin_addr.s_addr = inet_addr(str_IP);//将主机IP转换为网络IP
	server_addr.sin_port = htons(Port);//将主机端口转换为网络Port

	ret = connect(socket_fd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr_in));
	if(ret == -1)
	{
		printf("connect error\n");
		exit(1);
	}
	
	while(1)
	{
		//连接成功了,发送数据
		printf("Please input char:\n");     
		fgets(buff, 1024, stdin);   
		write(socket_fd, buff, strlen(buff)); 
	}
	
	close(socket_fd);
	return 0;
}

运行结果如下

注意

可通过nc指令测试服务器是否有误

Tags:

最近发表
标签列表