socket套接字编程——概述

HTTP/1.1 200 OK

基本原理

IP地址和端口号

IP地址用于区分不同的主机,而端口号则用于区分某个主机上不同的应用程序,端口号为0-65535,一般1024以下保留做系统使用。当我们进行socket编程时,必须区分端口号从而找到正确的应用程序。

socket通信过程

TCP(流式套接字SOCK_STREAM)

服务器端编程步骤

  1. 创建套接字(socket())
  2. 绑定套接字到一个IP地址和一个端口上(bind());
  3. 将套接字设置为监听模式等待连接请求(listen());
  4. 请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
  5. 用返回的套接字和客户端进行通信(send()/recv());
  6. 返回,等待另一连接请求;
  7. 关闭套接字(closesocket())。

客户端编程步骤

  1. 加载套接字库,创建套接字(WSAStartup()/socket());
  2. 向服务器发出连接请求(connect());
  3. 和服务器端进行通信(send()/recv());
  4. 关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。

UDP(报文式套接字SOCK_DGRAM)

服务器端编程步骤

  1. socket: 建立一个socket
  2. bind: 将这个socket绑定在某个端口上(AF_INET)
  3. recvfrom: 如果没有客户端发起请求,则会阻塞在这个函数里
  4. close: 通信完成后关闭socket

客户端编程步骤

  1. socket: 建立一个socket
  2. sendto: 向服务器的某个端口发起请求(AF_INET)
  3. close: 通信完成后关闭socket

原始套接字(SOCK_RAW)

常用API

socket()函数

声明

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

类似于文件的打开操作,socket创建一个socket描述符,通过该描述符进行一些读写操作。

参数

  • domain

指定协议族(family),决定了socket的地址类型。

名称 含义 名称 含义
PF_UNIX,PF_LOCAL 本地通信 PF_X25 ITU-T X25 / ISO-8208协议
AF_INET,PF_INET IPv4 Internet协议 PF_AX25 Amateur radio AX.25
PF_INET6 IPv6 Internet协议 PF_ATMPVC 原始ATM PVC访问
PF_IPX IPX-Novell协议 PF_APPLETALK Appletalk
PF_NETLINK 内核用户界面设备 PF_PACKET 底层包访问
  • type

指定socket类型

名称 含义
SOCK_STREAM Tcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM 支持UDP连接(无连接状态的消息)
SOCK_SEQPACKET 序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
SOCK_RAW RAW类型,提供原始网络协议访问
SOCK_RDM 提供可靠的数据报文,不过可能数据会有乱序
  • protocol

指定协议,常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

bind()函数

bind()函数将一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

声明

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

参数

sockfd,即要操作的socket描述字,socket()函数的返回值。

addr,一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。

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
// ipv4
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 */
};

// ipv6
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};

// Unix
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};

addrlen,对应地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。所以通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

listen()与connect()函数

声明

1
2
3
4
5
// listen    服务器端,用于监听套接字
int listen(int sockfd, int backlog);

// connect 客户端,用于发起连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数

  • listen

第一个参数sockfd即为要监听的socket描述字,第二个参数blacklog为相应socket可以排队的最大连接个数。
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

  • connect

第一个参数sockfd即为客户端的socket描述字,第二个参数addr为要连接的服务器的socket地址,第三个参数addrlen为服务器socket地址的长度。客户端通过调用connect()函数来建立与TCP服务器的连接。

accept()

在服务端开始监听套接字、客户端发起connect请求后,TCP服务器监听到该请求后,会调用accept()函数接收请求,之后可以开始网络IO操作,类似于普通文件读写。

声明

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

注意事项

accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

recvfrom()

UDP中接收客户端发起的请求,如果没有,那么会阻塞在这个函数中

sendto()

UDP中发送数据

实例

TCP套接字编程

server端

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cerrno>
#include <cstring>

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

int main() {
// 创建监听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
std::cout << "Error: socket" << std::endl;
return 0;
}
// 将socket与IP和端口号进行绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listenfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
std::cout << "Error: bind" << std::endl;
return 0;
}
// 监听套接字
if(listen(listenfd, 5) == -1) {
std::cout << "Error: listen" << std::endl;
return 0;
}
// 接受连接
int conn;
char clientIP[INET_ADDRSTRLEN] = "";
struct sockaddr_in clientAddr;
socklen_t clientAddrLen = sizeof(clientAddr);
while (true) {
std::cout << "...listening" << std::endl;
conn = accept(listenfd, (struct sockaddr*)&clientAddr, &clientAddrLen);
if (conn < 0) {
std::cout << "Error: accept" << std::endl;
continue;
}
inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, INET_ADDRSTRLEN);
std::cout << "...connect " << clientIP << ":" << ntohs(clientAddr.sin_port) << std::endl;

char buf[255];
while (true) {
memset(buf, 0, sizeof(buf));
int len = recv(conn, buf, sizeof(buf), 0);
buf[len] = '\0';
if (strcmp(buf, "exit") == 0) {
std::cout << "...disconnect " << clientIP << ":" << ntohs(clientAddr.sin_port) << std::endl;
break;
}
std::cout << buf << std::endl;
send(conn, buf, len, 0);
}

close(conn);
}
close(listenfd);
return 0;
}

client端

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
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cerrno>
#include <cstring>

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

int main() {
std::cout << "This is client" << std::endl;
// socket
int client = socket(AF_INET, SOCK_STREAM, 0);
if (client == -1) {
std::cout << "Error: socket" << std::endl;
return 0;
}
// connect
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8000);
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(client, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
std::cout << "Error: connect" << std::endl;
return 0;
}
std::cout << "...connect" << std::endl;
char data[255];
char buf[255];
while (true) {
std::cin >> data;
send(client, data, strlen(data), 0);
if (strcmp(data, "exit") == 0) {
std::cout << "...disconnect" << std::endl;
break;
}
memset(buf, 0, sizeof(buf));
int len = recv(client, buf, sizeof(buf), 0);
buf[len] = '\0';
std::cout << buf << std::endl;
}
close(client);
return 0;
}

UDP套接字编程

server端

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>   
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

#define SERV_PORT 8000

int main()
{
/* sock_fd --- socket文件描述符 创建udp套接字*/
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_fd < 0)
{
perror("socket");
exit(1);
}

/* 将套接字和IP、端口绑定 */
struct sockaddr_in addr_serv;
int len;
memset(&addr_serv, 0, sizeof(struct sockaddr_in)); //每个字节都用0填充
addr_serv.sin_family = AF_INET;             //使用IPV4地址
addr_serv.sin_port = htons(SERV_PORT);         //端口
/* INADDR_ANY表示不管是哪个网卡接收到数据,只要目的端口是SERV_PORT,就会被该应用程序接收到 */
addr_serv.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP地址
len = sizeof(addr_serv);

/* 绑定socket */
if(bind(sock_fd, (struct sockaddr *)&addr_serv, sizeof(addr_serv)) < 0)
{
perror("bind error:");
exit(1);
}


int recv_num;
int send_num;
char send_buf[20] = "i am server!";
char recv_buf[20];
struct sockaddr_in addr_client;

while(1)
{
printf("server wait:\n");

recv_num = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&addr_client, (socklen_t *)&len);

if(recv_num < 0)
{
perror("recvfrom error:");
exit(1);
}

recv_buf[recv_num] = '\0';
printf("server receive %d bytes: %s\n", recv_num, recv_buf);

send_num = sendto(sock_fd, send_buf, recv_num, 0, (struct sockaddr *)&addr_client, len);

if(send_num < 0)
{
perror("sendto error:");
exit(1);
}
}

close(sock_fd);

return 0;
}

client端

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>   
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#define DEST_PORT 8000
#define DSET_IP_ADDRESS "127.0.0.1"


int main()
{
/* socket文件描述符 */
int sock_fd;

/* 建立udp socket */
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
if(sock_fd < 0)
{
perror("socket");
exit(1);
}

/* 设置address */
struct sockaddr_in addr_serv;
int len;
memset(&addr_serv, 0, sizeof(addr_serv));
addr_serv.sin_family = AF_INET;
addr_serv.sin_addr.s_addr = inet_addr(DSET_IP_ADDRESS);
addr_serv.sin_port = htons(DEST_PORT);
len = sizeof(addr_serv);


int send_num;
int recv_num;
char send_buf[20] = "hey, who are you?";
char recv_buf[20];

printf("client send: %s\n", send_buf);

send_num = sendto(sock_fd, send_buf, strlen(send_buf), 0, (struct sockaddr *)&addr_serv, len);

if(send_num < 0)
{
perror("sendto error:");
exit(1);
}

recv_num = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr *)&addr_serv, (socklen_t *)&len);

if(recv_num < 0)
{
perror("recvfrom error:");
exit(1);
}

recv_buf[recv_num] = '\0';
printf("client receive %d bytes: %s\n", recv_num, recv_buf);

close(sock_fd);

return 0;
}

注意事项

区分监听套接字与已连接套接字

在服务器端中,我们实际上用到了两个socket,它们分别为:

  • 由listen函数将socket函数创建的socket转换而成的监听套接字,记作listenfd
  • 由accept函数等待来自客户端的连接请求到达listenfd,返回一个已连接套接字,记作connfd

两者区别如下:

监听套接字,是服务器作为客户端连接请求的一个端点,它被创建一次,并存在于服务器的整个生命周期。

已连接套接字是客户端与服务器之间已经建立起来了的连接的一个端点,服务器每次接受连接请求时都会创建一次已连接套接字,它只存在于服务器为一个客户端服务的过程中。

参考文献

0%