0%

socket相关命令

socket相关命令

学习《Linux高性能服务器编程》第五章Linux网络编程基础API,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中socket相关的命令,包括创建socket、命名socket、监听socket、接受连接、发起连接和关闭连接。

创建socket

socket使用系统调用可以创建一个socket

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

domain参数是告诉系统使用的是那个底层协议族,一般都是使用IPv4,所以使用AF_INET即可。关于socket系统调用支持的所有协议族,可以查看man手册(虽然参数名不一样,但是并无大碍)。

image-20220812095854483

type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_UGRAM(数据报)服务。对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。

image-20220812101617247

并且从Linux内核2.6.17起,增加了SOCK_NONBLOCKSOCK_CLOEXEC这两个标志值,表示将新创建的socket设为非阻塞,以及fork调用创建子进程时在子进程中关闭该socket。在Linux内核2.6.17前,需要调用fcntl进行设置。

protocol参数设置具体的协议。但是在前两个参数确定的情况下,这个参数的值基本上唯一的,所有几乎在所有情况下,我们都把这个值设置为0,表示使用默认协议。

socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。

1
2
3
4
5
6
7
8
9
#include <sys/socket.h>

int main(int argc, char const *argv[])
{
int lfd = 0;
lfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个 socket
close(lfd);
return 0;
}

命名socket

创建socket时,我们指定了地址族,但是并没有给定具体的地址,这样作为服务器别人是访问不到我们的。将一个socket 与socket地址绑定称为给socket命名。命名socket的系统调用是bind。

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

bindaddr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。

image-20220812104757237

bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errnoEACCESEADDRINUSE,它们的含义分别是:

  • EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时,bind将返回EACCES错误。
  • EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址。

image-20220812105430276

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>


#define SERV_PORT 8080

int main(int argc, char const *argv[])
{
int lfd = 0, cfd = 0;
int ret, i;

struct sockaddr_in serv_addr, clit_addr; // 定义服务器地址结构 和 客户端地址结构
socklen_t clit_addr_len; // 客户端地址结构大小

serv_addr.sin_family = AF_INET; // IPv4
serv_addr.sin_port = htons(SERV_PORT); // 转为网络字节序的 端口号
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 获取本机任意有效IP

lfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个 socket
if (lfd == -1)
{
perror("socket error");
}

bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); //给服务器socket绑定地址结构(IP+port)
close(lfd);
return 0;
}

监听socket

socket被命名后,还需要调用listen创建一个监听队列来存放处理的客户连接。

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

image-20220812112118847

sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。

在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket 的上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog参数的典型值是5。

image-20220812113608890

listen成功时返回0,失败则返回-1并设置erron

本来想测试backlog这个参数的效果,但是怎么也成功不了,不知道原因,以后有机会再进行尝试吧。

接受连接

接受连接通过accept进行

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

image-20220812143816774

sockfd指执行过listen的监听套接字的文件描述符。

addr是传出参数,用来获取接受连接的远端socket地址,地址的长度由addrlen参数指出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <arpa/inet.h>
#include <cstdio>
#include <string.h>
#include <cstdlib>
#include <assert.h>
#include <errno.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
if (argc <= 2)
{
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);

sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

int ret = bind(sock, (sockaddr *)&address, sizeof(address));
assert(ret != -1);

ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr *)&client, &client_addrlength);

if (connfd < 0)
{
printf("errno is: %d\n", errno);
}
else
{
char remote[INET_ADDRSTRLEN];
printf("connected with ip: %s and port: %d\n",
inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));
close(connfd);
}

close(sock);
return 0;
}

image-20220812151534158

并且书上面的实验说明了accept直接从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。比如:客户端在服务器accept之前就断网了,accept还是可以正常进行,它并不会返回错误。

发起连接

发动连接一般是客户端进行的,通过系统调用connect与服务器进行连接。

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

image-20220812154447716

sockfd参数由socket系统调用返回一个socketaddr参数是服务器监听的socket地址。addrlen参数指这个地址长度。

connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的errnoECONNREFUSEDETIMEDOUT,它们的含义如下:

  • ECONNREFUSED表示目标端口不存在,连接被拒绝。
  • ETIMEDOUT表示连接超时。

关闭连接

关闭连接一般来说使用

1
2
#include <unistd.h>
int close(int fd);

fd参数是待关闭的socket。不过,close并不会立即关闭这个连接,而是将fd的引用数量减1,直到fd引用数量为0,才真正关闭连接。在多进程程序中,一次fork系统调用默认将父进程中socket的引用计算加1,因此必须在子进程和父进程都对该socket进行close调用才能将连接关闭。

如果想立刻终止连接,直接调用shutdown

1
2
#include <sys/socket.h>
int shutdown(int sockfd, int how);

image-20220812160846669

sockfd参数是待关闭的socket,howto参数决定了shutdown的行为。

可选值 含义
SHUT_RD 关闭sockfd上读的这一半。应用程序不能再针对socket文件描述符执行读操作,并且该sockct接收缓冲区中的数据都被丢弃。
SHUT_WR 关闭sockfd上写的这一半。sockfd 的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该socket文件描述符执行写操作。这种情况下,连接处于半关闭状态。
SHUT_RDWR 同时关闭sockfd上的读和写。

可以看出shutdown可以灵活的关闭socket上的读或写。而close在关闭连接时只能将socket上的读和写同时关闭。

shutdown成功时返回0,失败则返回-1并设置errno