Linux Socket 编程(socket,bind,listen,accept,connect,write/read,sendto/recvfrom)



参考博客:https://www.jianshu.com/p/ca0bbd8700ce

Socket 这个词可以表示很多概念:

概念类别描述
1在TCP/IP协议中,【IP地址+TCP或UDP端口号】唯一标识网络通讯中的一个进程,【IP地址+端口号】就称为 Socket
2在TCP协议中,建立连接的两个进程各自有一个 Socket 来标识,那么这两个Socket 组成的socket pair就唯一标识一个连接。
Socket 本身有 插座 的意思,因此用来描述网络连接的一对一关系。
3TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的 应用层编程接口 称为 Socket API

1、预备知识

1.1 网络字节序

在几乎所有的计算机上,多字节的对象,都被表示为连续的字节序列。

数据的高字节 保存在 内存的低地址,称为大端模式(大端序);
数据的高字节 保存在 内存的高地址,称为小端模式(小端序)。

0x12345678 这样一个 32 位整数在内存中需要占用四个字节,这四个字节的地址会递增。
若随着地址增加,按照 0x12、0x34、0x56 和 0x78 这样的顺序存入内存,就称为大端序
反之,若随着地址增加,按照 0x78、0x56、0x34、0x12 的顺序存入内存,就称为小端序

大端序和小端序内存布局示意图:
在这里插入图片描述
在网络应用中,字节序是一个必须考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均须按照网络标准转化。

网络传输的标准叫做 网络字节序,实际上是大端序
而我们常用的 X86都是小端序,ARM 的字节序实际上是可配置的,但是一般都配置为小端。

在网络编程中不应该假设自己程序运行的主机的字节序,应当使用 htonl/htons/ntohs/ntohl 之类的函数来在网络字节序主机字节序之间进行转换。

#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,表示网络上传输的字节序;sl代表类型shortlong


1.2 客户端/服务器模型

网络上进行通信的各端点,大部分都是遵循 客户端/服务器 模型的。

一般来说,服务器端具有以下特征:

序号特征
1被动通信
2始终等待来自客户端的请求
3自己参与通信的网络接口端口必须确定
4处理客户端请求后,将结果(响应)返回给客户端

客户端的特征如下:

序号特征
1主动通信
2需要发起请求
3自己参与通信的网络接口端口可以不确定
4发起请求后,需要等待服务器回应结果

服务器可以是有状态的,也可以是无状态的(Stateless),无状态的服务器不会保留两个请求之间的任何信息,而有状态的服务器会记住请求之间的信息。
实际上的服务器一般都能同时并发处理多个客户端的请求。

一个简单的客户机/服务器通信过程如图:
在这里插入图片描述


2、编程接口 BSD Socket

2.1 Socket是什么

TCP/IP协议 存在于OS中,网络服务通过 OS 提供,在OS中增加支持TCP/IP的系统调用——Berkeley套接字,如 socket,connect,send,recv等。


网络传输概貌:
在这里插入图片描述
Socket 是应用层TCP/IP协议族通信的中间软件抽象层
在这里插入图片描述
Socket 起源于Unix,而Unix/Linux 基本哲学之一就是一切皆文件,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。
Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

注意:其实Socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,大量用的都是通过Socket实现的。


2.2 基本的 Socket 编程接口

现在的网络编程接口通常是 Socket。
BSD Socket 是事实上的网络应用编程接口标准(API),其它编程语言往往也使用和这套用 C 写成的 Socket 类似的接口。

用 Socket 能够实现网络上的不同主机之间同一主机不同对象之间的数据通信。所以,现在 Socket 已经是一类通用通信接口集合

2.2.1 Socket API 概览

在这里插入图片描述
服务器端 先创建 socket(),然后与端口绑定 bind(),对端口进行监听 listen(),调用 accept() 阻塞,等待客户端连接。在这时如果有个客户端初始化一个 socket(),然后连接服务器 connect(),如果连接成功,这时客户端与服务器端的连接就建立了。

客户端 发送数据请求,服务器端 接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
这些接口的实现都是内核来完成。具体如何实现,可以看看 Linux内核。

序号函数原型描述
1int socket(int domain,int type,int protocol)创建 Socket
2int bind(int socket,const struct sockaddr *address,socklent address_len)绑定地址端口
3int listen(int socket,int backlog)设置 Socket 为监听模式
4int accept(int socket,struct sockaddr *restrict address,socklen_t *restrict address_len接受连接
5int connect(int socket,const struct sockaddr *address,socklent address_len连接服务器
(1)socket():创建 Socket

在进行 Socket 通信之前,一般调用 socke(2) 函数来创建一个 Socket 通信端点。
socket() 打开一个网络通讯端口,如果成功的话,就像 open() 一样返回一个文件描述符,应用程序可以像读写文件一样用 read()/write() 在网络上收发数据,如果 socket() 调用出错则返回-1。

对于IPv4,domain参数指定为AF_INET。
对于TCP协议,type 参数指定为SOCK_STREAM,表示 面向流的传输协议。如果是UDP协议,则 type 参数指定为SOCK_DGRAM,表示 面向数据报的传输协议。

socket(2) 函数原型如下:

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

int socket(int domain, int type, int protocol);
序号参数含义
1domain地址类型:
(1)AF_INET(IPv4)
(2)AF_INET6(IPv6)
2type协议类型:
(1)SOCK_STREAM(面向连接)
(2)SOCK_DGRAM(非连接)
3protocol默认填0

如果建立套接字成功,返回一个新的文件描述符,失败则返回-1,设置errno。这个函数可能发生的错误:

序号errno 值错误含义
1EPROTONOSUPPORT参数 domain 指定的类型不支持参数 type 或 protocol 指定的协议
2ENFILE核心内存不足,无法建立新的 socket 结构
3EMFILE进程文件表溢出,无法再建立新的套接字
4EACCESS权限不足,无法建立 type 或 protocol 指定的协议
5ENOBUFS、ENOMEM内存不足
6EINVAL参数不合法(invalid)

序号类型示例
1创建 TCP Socketsock_fd = socket(AF_INET, SOCK_STREAM, 0);
2创建 UDP Socketsock_fd = socket(AF_INET, SOCK_DGRAM, 0);

实际程序中,应该先检查返回值 sock_fd 有效后,再使用。


(2)bind():绑定地址和端口

创建了 Socket 后,可以调用 bind(2) 函数来将这个 Socket 绑定到特定的地址端口上来进行通信。函数原型如下:

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

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

参数列表中,socket 应该是一个指向 Socket 的有效文件描述符

序号参数含义
1sockfdsocket 文件描述符
2addr构造出 IP地址 加 端口号
3addrlensizeof(addr)长度

(1)addr 参数就是一个指向struct sockaddr结构的指针,根据不同的协议,可以有不同的具体结构,对于 IP 地址,就是struct sockaddr_in。但是在调用函数的时候需要强制转换一下这个指针来避免警告。
(2)address_len 因为前面的地址可能有各种不同的地址结构,所以,此处应该指明所使用的地址数据结构的长度。编程时直接取sizeof(struct sockaddr_in)即可。

当 bind(2) 调用成功时返回 0失败时返回 -1,这时需要检查的 errno 值 见下表:

序号errno 值错误含义
1EADDRINUSE指定的地址和端口 已经被占用
2EADDRNOTAVAIL本机不存在指定的地址
3EAFNOSUPPORT对于指定的地址族来说,地址无效
4ENOTSOCKsocket 不是指向 socket 的 文件描述符
5EBADFsocket 不是有效的文件描述符
6EINVALsocket 已经绑定到一个地址,协议不支持绑定到新地址,或者 socket 已关闭
7EOPNOTSUPPsocket 类型不支持对地址的绑定

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

对于 客户端程序,一般来说可以不用显式 bind(2),协议栈会在发起通信时将 Socket 自动绑定到一个随机的可用端口上进行通信,但是显式 bind(2) 也是可以的。

bind() 的作用是将参数sockfdaddr绑定在一起,使 sockfd 这个用于 网络通讯的文件描述符 监听 addr 所描述的 地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr 参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。

通常 服务器程序 使用 bind() 绑定端口的流程如下:

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(SERV_PORT);

bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

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

(3)listen():设置 Socket 为监听模式
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);
序号参数含义
1sockfdSocket 文件描述符
2backlogSocket 可以排队的最大连接个数

listen() 成功返回0,失败返回-1。

查看系统默认backlog:

cat /proc/sys/net/ipv4/tcp_max_syn_backlog	#128

典型的 服务器程序 可以同时服务于 多个客户端,当有客户端发起连接时,服务器调用的 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);
序号参数含义
1sockfdSocket 文件描述符
2addr传出参数,返回连接客户端地址信息,含 IP地址 和 端口号
3addrlen传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时 返回真正接收到 地址结构体的大小

返回值:成功返回一个新的 Socket 文件描述符,用于和客户端通信,失败返回-1,设置errno。

三方握手完成后,服务器调用 accept() 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就 阻塞等待 直到有 客户端连接上来。

addr 是一个传出参数,accept() 返回时传出 客户端的地址和端口号。
addrlen 参数是一个传入传出参数(value-result argument),传入的是调用者提供的 缓冲区addr的长度 以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。

如果给addr参数传NULL,表示不关心客户端的地址。

我们的 服务器程序 结构是这样的:

while (1) {
	cliaddr_len = sizeof(cliaddr);
	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
	n = read(connfd, buf, MAXLINE);
	......
	close(connfd);
}

整个是一个while死循环,每次循环处理一个客户端连接。由于 cliaddr_len 是传入传出参数,每次调用 accept() 之前应该重新赋初值。
accept() 的参数 listenfd 是先前的监听文件描述符,而 accept() 的返回值是另外一个文件描述符 connfd,之后与客户端之间就通过这个 connfd 通讯,最后关闭 connfd 断开连接,而不关闭 listenfd,再次回到循环开头 listenfd 仍然用作 accept 的参数。

accept() 成功返回一个文件描述符,出错返回-1。

(5)connect():连接服务器
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
序号参数含义
1sockfdsocket 文件描述符
2addr传入参数,指定服务器端地址信息,含IP地址和端口号
3addrlen传入参数,传入sizeof(addr)大小

返回值:成功返回0,失败返回-1,设置errno。

客户端需要调用 connect() 连接服务器,connect() 和 bind() 的参数形式一致,区别在于 bind() 的参数是自己的地址,而 connect() 的参数是对方的地址。
connect() 成功返回0,出错返回-1。


3、面向连接的 Socket:C/S模型-TCP

下图是基于TCP协议的 客户端/服务器 程序的一般流程:
在这里插入图片描述
服务器 调用 socket()、bind()、listen() 完成初始化后,调用 accept() 阻塞等待,处于监听端口的状态,客户端调用 socket() 初始化后,调用 connect() 发出 SYN段 并阻塞等待 服务器应答,服务器应答一个 SYN-ACK段,客户端收到后从 connect() 返回,同时应答一个 ACK段,服务器收到后从 accept() 返回。

数据传输的过程:

建立连接后,TCP协议提供 全双工的通信服务,但是一般的 客户端/服务器程序 的流程是由 客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从 accept() 返回后立刻调用 read(),读socket 就像读管道一样,如果没有数据到达就 阻塞等待,这时客户端调用 write() 发送请求给服务器,服务器收到后从 read() 返回,对客户端的请求进行处理,在此期间 客户端 调用 read() 阻塞等待 服务器 的应答,服务器 调用 write() 将处理结果发回给 客户端,再次调用 read() 阻塞等待下一条请求,客户端收到后从 read() 返回,发送下一条请求,如此循环下去。

如果 客户端 没有更多的请求了,就调用 close() 关闭连接,就像写端关闭的管道一样,服务器的 read() 返回0,这样 服务器 就知道 客户端 关闭了连接,也调用 close() 关闭连接。

注意,任何一方调用 close() 后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用 shutdown() 则连接处于 半关闭状态,仍可接收对方发来的数据。

在学习 Socket API 时要注意应用程序和TCP协议层是如何交互的: 应用程序调用某个Socket函数时TCP协议层完成什么动作,比如调用 connect() 会发出 SYN段。应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的Socket函数返回就表明TCP协议收到了某些段。再比如 read() 返回0 就表明收到了 FIN段

3.1 server

下面通过最简单的 客户端/服务器程序 的实例来学习 Socket API。
server.c的作用是从 客户端读字符,然后将每个字符转换为大写并回送给 客户端。

/* 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_PORT 8000
int main(void)
{
	struct sockaddr_in servaddr, cliaddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i, n;
	
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
	
	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	
	listen(listenfd, 20);
	
	printf("Accepting connections ...\n");
	
	while (1) {
		cliaddr_len = sizeof(cliaddr);
		connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
		n = read(connfd, buf, MAXLINE);
		printf("received from %s at PORT %d\n",
		inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
		ntohs(cliaddr.sin_port));
		
		for (i = 0; i < n; i++)
			buf[i] = toupper(buf[i]);
		write(connfd, buf, n);
		close(connfd);
	}
}

3.2 client

client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收 服务器返回的字符串并打印。

/* client.c */
#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 8000

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;
	char *str;
	
	if (argc != 2) {
		fputs("usage: ./client message\n", stderr);
		exit(1);
	}
	str = argv[1];
	
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);
	
	connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	
	write(sockfd, str, strlen(str));
	
	n = read(sockfd, buf, MAXLINE);
	printf("Response from server:\n");
	write(STDOUT_FILENO, buf, n);
	
	close(sockfd);
	return 0;
}

由于 客户端 不需要固定的端口号,因此不必调用 bind(),客户端的端口号由 内核 自动分配。注意,客户端不是不允许调用 bind(),只是没有必要调用 bind() 固定一个端口号。

服务器 也不是必须调用 bind(),但如果服务器不调用 bind(),内核 会自动给 服务器分配 监听端口,每次启动服务器时端口号都不一样,客户端 要连接 服务器就会遇到麻烦。

客户端和服务器 启动后可以查看连接情况:

netstat -apn | grep 8000

4、面向无连接的 Socket:C/S模型-UDP

在这里插入图片描述
由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上 有很多 保证通讯可靠性的机制 需要在 应用层 实现。

编译运行 server,在两个终端里各开一个 client 与 server 交互,看看 server 是否具有 并发服务 的能力。用 Ctrl+C 关闭 server,然后再运行server,看此时client还能否和server联系上。和前面TCP程序的运行结果相比较,体会无连接的含义。

4.1 server

/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 8000

int main(void)
{
	struct sockaddr_in servaddr, cliaddr;
	socklen_t cliaddr_len;
	int sockfd;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i, n;
	
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family 	 = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port 		 = htons(SERV_PORT);
	
	bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	
	printf("Accepting connections ...\n");
	while (1) {
		cliaddr_len = sizeof(cliaddr);
		n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);
		if (n == -1)
			perr_exit("recvfrom error");
			
		printf("received from %s at PORT %d\n",
			inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
			ntohs(cliaddr.sin_port));
		
		for (i = 0; i < n; i++)
			buf[i] = toupper(buf[i]);
		n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
		if (n == -1)
			perr_exit("sendto error");
	}
}

4.2 client

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	int sockfd, n;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	socklen_t servaddr_len;
	
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);
	
	while (fgets(buf, MAXLINE, stdin) != NULL) {
		n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
		if (n == -1)
			perr_exit("sendto error");
			
		n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);
		if (n == -1)
			perr_exit("recvfrom error");
			
		write(STDOUT_FILENO, buf, n);
	}
	
	close(sockfd);
	return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值