网络编程之socket编程基础

网络编程之socket编程基础,介绍简单的socket基础知识,以及socket的使用方法。

什么是Socket?

  • socket可以看成是用户进程与内核网络协议栈的编程接口。
  • socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信。
  • socket是全双工通信,也可以用于异构平台通信,例如手机RAM和PCx86之间。

IPv4套接子地址结构

  • IPv4套接字地址结构通常也称为“网络套接字地址结构”,它以“sockaddr_in”命名,定义在头文件中。
1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in {
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
}
struct in_addr {
in_addr_t s_addr;
}
  • sin_len:整个sockaddr_in结构体的长度
  • sin_family:指定该地址家族,在这里必须设置为AF_INET
  • sin_port:端口
  • sin_addr:IPv4的地址
  • sin_zero:暂不使用,一般将其置为0

通用地址结构

  • 通用地址结构用来指定与套接字关联的地址

    1
    2
    3
    4
    5
    struct sockaddr {
    uint8_t sin_len;
    sa_family_t sin_family;
    char sa_data[14];
    }
  • sin_len:整个sockaddr_in结构体的长度

  • sin_family:指定该地址家族
  • sa_data:由sin_family决定它的形式

网络字节序

字节序

  • 大端字节序(Big Endian):最高有效位(MSB:Most Significant Bit)存储于最低内存地址处,最低有效位(LSB:Lowest Significant Bit)存储于最高内存地址处。
  • 小端字节序(Little Endian): 最高有效位(MSB:Most Significant Bit)存储于最高内存地址处,最低有效位(LSB:Lowest Significant Bit)存储于最低内存地址处。

主机字节序

不同主机有不同的字节序,如x86为小端字节序,Motorola 6800为大端字节序,ARM字节序是可配置的。

网络字节序

网络字节序规定为大端字节序。

实例1:测试当前平台字节序

1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
unsigned int x = 0x12345678;
unsigned char *p = (unsigned char*)&x;
printf("%0x, %0x, %0x, %0x\n", p[0], p[1], p[2], p[3]);
return 0;
}

字节序转换函数

1
2
3
4
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,s代表short,l代表long

地址转换函数

1
2
3
4
5
6
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);

实例2:地址转换函数用法

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <arpa/inet.h>
int main() {
unsigned long addr = inet_addr("192.168.0.100");
printf("addr=%u\n", ntohl(addr));
struct in_addr ipaddr;
ipaddr.s_addr = addr;
printf("%s\n", inet_ntoa(ipaddr));
return 0;
}


套接字类型

  • 流式套接字(SOCK_STREAM)
    提供面向连接的、可靠地数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
  • 数据报式套接字(SOCK_DGRAM)
    提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接受顺序混乱。
  • 原始套接字(SOCK_RAW)

socket()函数

1
2
#inlcude <sys/socket.h>
int socket(int domain, int type, int protocol);

功能:创建一个套接字用于通信
参数:

  • domain:指定通信协议簇(protocol family)
  • type:指定socket类型,流式套接字SOCK_STREAM,数据报套接字SOCK_DGRAM,原始套接字SOCK_RAM
  • protocol:协议类型

返回值:成功返回非负整数,它与文件描述符类似,我们把他称作为套接字描述符,简称套接字。失败返回-1。


bind()函数

1
2
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socketlen_t addrlen);

功能:绑定一个本地地址到套接字
参数:

  • sockfd:socket函数返回的套接字
  • addr:要绑定的地址
  • addrlen:地址长度

返回值:成功返回0,失败返回-1


listen()函数

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

功能:将套接字用于监听进入的连接
参数:

  • sockfd:socket函数返回的套接字
  • backlog:规定内核为此套接字排队的最大连接个数

返回值:成功返回0,失败返回-1

一般来说,listen函数应该在调用socket和bind函数之后,调用函数accept之前调用。
对于给定的监听套接字,内核要维护两个队列:

  • 1、已由客户发出并到达服务器,服务器正在等待完成相应的TCP三次握手过程
  • 2、已完成连接的队列

accept()函数

1
2
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
参数:

  • sockfd:服务器套接字
  • addr:将返回对等方的套接字地址
  • addrlen:返回对等方的套接字地址长度

返回值:成功返回非负整数,失败返回-1


1
2
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能:建立一个连接至addr所指定的套接字
参数:

  • sockfd:指定的套接字
  • addr:要连接的套接字地址
  • addrlen:第二个参数的长度

返回值:成功返回0,失败返回-1


简单的服务器客户端模型

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
/*server.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define ERR_EXIT(m) \
do{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
int listenfd;
if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// equal with
/* if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
ERR_EXIT("socket");
}*/
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(5188);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
// serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &serveraddr.sin_addr.s_addr);
if (bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
ERR_EXIT("bind");
}
if (listen(listenfd, SOMAXCONN) < 0) {
ERR_EXIT("listen");
}
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn;
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) {
ERR_EXIT("accept");
}
char recvbuf[1024];
while (1) {
memset(recvbuf, 0, sizeof(recvbuf));
read(conn, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
write(conn, recvbuf, sizeof(recvbuf));
}
close(conn);
close(listenfd);
return 0;
}
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
/*client.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define ERR_EXIT(m) \
do{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
int sock;
if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(5188);
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
ERR_EXIT("connect");
}
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while (fgets(sendbuf, sizeof(sendbuf), stdin) != 0) {
write(sock, sendbuf, sizeof(sendbuf));
read(sock, recvbuf, sizeof(recvbuf));
fputs(sendbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
return 0;
}

REUSEADDR选项

  • 服务器端尽可能使用REUSEADDR选项。
  • 在绑定之前可能调用setsockopt来设置REUSEADDR套接字选项。
  • 使用REUSEADDR选项可以不必等待TIME_WAIT状态消失就可以重启服务器。
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 <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#define ERR_EXIT(m) \
do{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main() {
int listenfd;
if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
ERR_EXIT("socket");
}
// equal with
/* if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
ERR_EXIT("socket");
}*/
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(5188);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
// serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &serveraddr.sin_addr.s_addr);
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
ERR_EXIT("setsockopt");
}
if (bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
ERR_EXIT("bind");
}
if (listen(listenfd, SOMAXCONN) < 0) {
ERR_EXIT("listen");
}
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);
int conn;
if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) {
ERR_EXIT("accept");
}
printf("ip=%s, port=%d\n", inet_ntoa(peeraddr.sin_addr);, ntohs(peeraddr.sin_port));
char recvbuf[1024];
while (1) {
memset(recvbuf, 0, sizeof(recvbuf));
read(conn, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
write(conn, recvbuf, sizeof(recvbuf));
}
close(conn);
close(listenfd);
return 0;
}