2、Socket编程

一、套接字的概念

socket本身又“插座”的意思,所以提到socket都是成对出现的。
在这里插入图片描述
IP地址:在网络环境中唯一标识一台主机
端口号:在主机中唯一标识一个进程
IP+端口号:在网络环境中唯一标识一个进程, 这个进程就是socket

因此, socket一定要有IP和端口号

套接字是Linux文件的一种类型:本质为内核借助缓冲区形成的伪文件。所以socket是一个文件,但是不占据内存空间。

一共有七种文件类型
C 字符设备
B 块设备
P 管道
S 套接字

一个文件描述符指向两块缓冲区,因此可以同时读写,双向全双工。
套接字通信原理:
根据IP地址和端口号对sfd和cfd进行通信
在这里插入图片描述
总结:
1、socket成对出现
2、绑定IP+端口
3、一个文件描述符指向两个缓冲区
一个缓冲区读,一个缓冲区写

二、预备知识

1、网络字节序

数据要传输都需要转换成二进制。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h表示host,n表示network,l表示32位长整数,s表示16位短整数。
32位IP,16位端口号。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

2、IP地址转换

点分十进制

#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); 

参数:
int af
AF_INET 代表IPv4
AF_INET6 代表IPv6

支持IPv4和IPv6

这个函数比较方便, 不然IP要先转成unsing int,再转成网络字节序

3、sockaddr数据结构

(1)强制转换类型

在这里插入图片描述
因为历史遗留问题, 要向前兼容。
所以定义新的结构体 ,函数内部再强制类型转化为所需的地址类型。
比如bind函数:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
        socklen_t addrlen);

给bind传参:
struct sockaddr_in addr;
bind( listen_fd, (struct sockaddr *)&addr,sizeof(addr) );
类似函数还有:accept(); connect();

(2)sockaddr数据结构:

使用 sudo grep -r "struct sockaddr_in {" /usr 命令可查看到struct sockaddr_in结构体的定义。一般其默认的存储位置:/usr/include/linux/in.h 文件中。
或者 man 7 ip命令查看。

struct sockaddr {
	sa_family_t sa_family; 		/* address family, AF_xxx */
	char sa_data[14];			/* 14 bytes of protocol address */
};
struct sockaddr_in {
	__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)];
};
struct in_addr {						/* Internet address. */
	__be32 s_addr;
};
struct sockaddr_in6 {
	unsigned short int sin6_family; 		/* AF_INET6 */
	__be16 sin6_port; 					/* Transport layer port # */
	__be32 sin6_flowinfo; 				/* IPv6 flow information */
	struct in6_addr sin6_addr;			/* IPv6 address */
	__u32 sin6_scope_id; 				/* scope id (new in RFC2553) */
};
struct in6_addr {
	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 */
};

(3)总结:

     struct sockaddr_in {
               sa_family_t    sin_family; /* address family: AF_INET */
               in_port_t      sin_port;   /* port in network byte order */
               struct in_addr sin_addr;   /* internet address */
           };

           /* Internet address. */
           struct in_addr {
               uint32_t       s_addr;     /* address in network byte order */
           };
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6;
addr.sin_port = htons() / ntohs();//端口
addr.sin_addr.s_addr = htonl() / inet_pton() / inet_ntop(); //IP地址

三、网络套接字函数

在这里插入图片描述
TCP客户端其实也有绑定IP和端口号, 只是说它可以是随机的所以没有画出来。 而服务器是不能随机的 。

服务器 read write 返回的是accept的新的文件描述符。

1、socket函数——打开一个网络通讯端口

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:
	AF_INET: 用IPv4的地址
	AF_INET6:用IPv6的地址
type:
	SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这个socket是使用TCP来进行传输。
	SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
	SOCK_RAW 
	SOCK_RDM
protocol:0 表示使用默认协议。
	流式协议 protocol=0 默认为TCP
	报式协议 protocol=0 默认为UDP
返回值:
	成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno

2、bind函数——把套接字和固定的网络地址和端口号绑定

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
固定的网络地址和端口号 放在struct sockaddr * 里。

#include <sys/types.h> /* See NOTES */
#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));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);

上面的代码解析:
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。

3、listen函数——指定监听数

服务器要等待客户端连接,连接建立好了后再进行数据传递。 listen函数用来指定同时允许多少个客户端与我建立连接。
例子:假如有400个客户端要与我建立连接, 第401个客户端就只能等400个客户端中有一个连接建立好了,第401个才能与我连接。

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:
	socket文件描述符
backlog:
	排队建立3次握手队列和刚刚建立3次握手队列的链接数和
返回值:
	成功返回0,失败返回-1

查看系统默认backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
当有客户端发起连接时,服务器调用的accept()返回并接受这个连接。尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。

4、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文件描述符,这个新的文件描述符不是之前socket函数建立的,它用于和客户端通信,失败返回-1,设置errno

addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。

5、connect函数——客户端调的函数,连接服务器

#include <sys/types.h> 					/* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
	socket文件描述符, 是客户端自己的fd
addr:
	传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
	传入参数,传入sizeof(addr)大小
返回值:
	成功返回0,失败返回-1,设置errno

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是服务器自己的地址,而connect的参数是服务器的地址。

四、C/S模型-TCP —— 编写代码

写一个客户端传小写, 服务器变成大写的代码流程:
server.c
1、socket() 建立套接字
2、bind() 绑定IP端口号 (struct sockaddr_in addr)
3、listen() 指定最大同时发起连接数
4、accept() 阻塞等待客户端发起连接
5、read()
6、小写–大写
7、write给客户端
8、close();
client.c
1、socket();
2、bind(); 可要可不要,可以依赖“隐式绑定”
3、connect();
4、write();
5、read();
6、close();

1、server

server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。

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

#define MAXLINE 80
#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666 //定义3000以上

int main()
{
	struct sockaddr_in seraddr, cliaddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	char buf[MAXLINE]; //char buf[BUFSIZE];
	char str[INET_ADDRSTRLEN];
	int i, n;
	
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	
	seraddr.sin_family = AF_INET;
	addr.sin_port = htons(SERV_PORT);
	addr.sin_addr.s_addr = inet_pton(SERV_IP);
	//addr.sin_addr.s_addr = htonl(INADDR_ANY);
	bind(listenfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
	
	listen(listenfd, 32); //默认值是128
	
	cliaddr_len = sizeof(cliaddr_len);
	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
	
	while(1)
	{
		n = read(connfd, buf, sizeof(buf));
		for(i=0; i<n; i++)
		{
			buf[i] = toupper(buf[i]); //tolower()小写
		}
		write(connfd, buf, n);
	}
	close(listenfd);
	close(connfd);
}	

编译:

gcc serve.c -o server -Wall -g

新发现:vim server.c +30 就可以跳转到那一行
K 可以跳转man手册

还没编写客户端的程序,我们可以用过nc(net connect)命令测试:

nc 127.0.01 6666  

在这里插入图片描述

2、client

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 80
#define SERV_PORT 6666
#define SERV_IP 192.168.100

int main()
{
	struct sockaddr_in seraddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	char buf[MAXLINE]; //char buf[BUFSIZE];
	char str[INET_ADDRSTRLEN];
	int i, n;
	
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	
	seraddr.sin_family = AF_INET;
	addr.sin_port = htons(SERV_PORT);
	addr.sin_addr.s_addr = inet_pton(AF_INET, SERV_IP, &sockaddr_in.in_addr.sin_addr);
	//addr.sin_addr.s_addr = htonl(INADDR_ANY);

	
	listen(listenfd, 32); //默认值是128
	
	cliaddr_len = sizeof(cliaddr_len);
	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
	
	while(1)
	{
		n = read(connfd, buf, sizeof(buf));
		for(i=0; i<n; i++)
		{
			buf[i] = toupper(buf[i]); //tolower()小写
		}
		write(connfd, buf, n);
	}
	close(listenfd);
	close(connfd);
}	

3、出错处理

#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s)
{
	perror(s);
	exit(1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
	int n;
	again:
	if ( (n = accept(fd, sa, salenptr)) < 0) {
		if ((errno == ECONNABORTED) || (errno == EINTR))
			goto again;
		else
			perr_exit("accept error");
	}
	return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
	int n;
	if ((n = bind(fd, sa, salen)) < 0)
		perr_exit("bind error");
	return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
	int n;
	if ((n = connect(fd, sa, salen)) < 0)
		perr_exit("connect error");
	return n;
}
int Listen(int fd, int backlog)
{
	int n;
	if ((n = listen(fd, backlog)) < 0)
		perr_exit("listen error");
	return n;
}
int Socket(int family, int type, int protocol)
{
	int n;
	if ( (n = socket(family, type, protocol)) < 0)
		perr_exit("socket error");
	return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
	ssize_t n;
again:
	if ( (n = read(fd, ptr, nbytes)) == -1) {
		if (errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
	ssize_t n;
again:
	if ( (n = write(fd, ptr, nbytes)) == -1) {
		if (errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}
int Close(int fd)
{
	int n;
	if ((n = close(fd)) == -1)
		perr_exit("close error");
	return n;
}
ssize_t Readn(int fd, void *vptr, size_t n)
{
	size_t nleft;
	ssize_t nread;
	char *ptr;

	ptr = vptr;
	nleft = n;

	while (nleft > 0) {
		if ( (nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;
			else
				return -1;
		} else if (nread == 0)
			break;
		nleft -= nread;
		ptr += nread;
	}
	return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
	size_t nleft;
	ssize_t nwritten;
	const char *ptr;

	ptr = vptr;
	nleft = n;

	while (nleft > 0) {
		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
			if (nwritten < 0 && errno == EINTR)
				nwritten = 0;
			else
				return -1;
		}
		nleft -= nwritten;
		ptr += nwritten;
	}
	return n;
}

static ssize_t my_read(int fd, char *ptr)
{
	static int read_cnt;
	static char *read_ptr;
	static char read_buf[100];

	if (read_cnt <= 0) {
again:
		if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return -1;	
		} else if (read_cnt == 0)
			return 0;
		read_ptr = read_buf;
	}
	read_cnt--;
	*ptr = *read_ptr++;
	return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
	ssize_t n, rc;
	char c, *ptr;
	ptr = vptr;

	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c == '\n')
				break;
		} else if (rc == 0) {
			*ptr = 0;
			return n - 1;
		} else
			return -1;
	}
	*ptr = 0;
	return n;
}

read 返回值:
1、 >0 实际读到的字节数 buf=1024
(1)== buf1024
(2)<buf 56;
2、 = 0 数据读完(读到文件、管道、socket末尾-对端关闭)
3、-1 异常
(1)errno == EINTR 被信号中断 重启/quit
(2)errno == EAGAIN(EWOULDBLOCK) 非阻塞方式读,并且没有数据(3)其他值 出现错误–perror exit。

发布了56 篇原创文章 · 获赞 3 · 访问量 2369

猜你喜欢

转载自blog.csdn.net/qq_40674996/article/details/102629179