1 预备知识
1.1 网络字节序
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678
大端法:(网络存储) 高位存低地址。地位存高地址。
//192.168.1.11 --> string --> atoi --> int -->htonl --> 网络字节序
htonl() -->本地-->网络 (IP)
htons() --> 本地--》网络 (port)
ntohl() --> 网络--》 本地(IP)
ntohs() --> 网络--》 本地(Port)
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
#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);
1.2 IP地址转换函数
#include <arpa/inet.h>
//本地字节序(string IP) ---> 网络字节序
int inet_pton(int af, const char *src, void *dst);
参数:
af:AF_INET(IPV4)、AF_INET6、AF_UNIX
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
//网络字节序 ---> 本地字节序(string IP)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
返回值:
成功:dst。
失败:NULL
特点
- 支持IPv4和IPv6
- 可重入函数
- 其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr。因此函数接口是void *addrptr。
2 C/S通信流程
server:
1. socket() 创建socket
2. bind() 绑定服务器地址结构
3. listen() 设置监听上限
4. accept() 阻塞监听客户端连接
5. read(fd) 读socket获取客户端数据
6. 小--大写 toupper()
7. write(fd)
8. close();
client:
1. socket() 创建socket
2. connect(); 与服务器建立连接
3. write() 写数据到 socket
4. read() 读转换后的数据。
5. 显示读取结果
6. close()
2.1 服务器端
2.1.1 sockaddr地址结构
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
struct sockaddr {
sa_family_t sa_family; //选择地址家族,AF_xxx | 例如是IPV4的就是选择AF_INET | AF_INET6
char sa_data[14]; // 地址数据
}
struct sockaddr_in {
sa_family_t sin_family; // 地址家族: AF_INET
in_port_t sin_port; // 两字节的端口号(网络字节序)
struct in_addr sin_addr; // 因特网地址
}
// 因特网地址
struct in_addr {
uint32_t s_addr; //32位的网络字节顺序的IP地址(32位的无符号整型数)
// 而且这个整型数要转换成字符串
// 再如,若客户端要去连接某个IP地址,要将字符串转换成整型数
}
2.1.2 socket函数
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:
传0 表示使用默认协议。
返回值:
成功:返回指向新创建的socket的文件描述符
失败:返回-1,设置errno
2.1.3 bind函数
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
#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
bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:
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);
2.1.4 listen函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:
socket文件描述符
backlog:
排队建立3次握手队列和刚刚建立3次握手队列的链接数和
查看系统默认backlog
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
2.1.5 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文件描述符,用于和客户端通信
失败返回-1,设置errno
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
2.2 客户端
2.2.1 connect函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功返回 0
失败返回-1,设置errno
//addr参数传入时注意事项
struct sockaddr_in srv_addr; // 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 端口号 //跟服务器bind时设定的 port 完全一致。
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
3 简易实现代码
3.1 服务器端
1 #include "stdio.h"
2 #include "stdlib.h"
3 #include "string.h"
4 #include "unistd.h"
5 #include "errno.h"
6 #include "pthread.h"
7 #include "sys/types.h"
8 #include "sys/socket.h"
9 #include "arpa/inet.h"
10 #include "ctype.h"
11
12 void sys_err(const char *str)
13 {
14 perror(str);
15 exit(1);
16 }
17
18 int main(int argc, char *argv[])
19 {
20 //创建一个socket,用文件描述符接收
21 int sockFd = socket(AF_INET, SOCK_STREAM, 0);
22 if(-1 == sockFd)
23 {
24 sys_err("socket error");
25 }
26
27 //给socket绑定一个地址结构 IP + port
28 struct sockaddr_in serv_addr;
29 serv_addr.sin_family = AF_INET;
30 serv_addr.sin_port = htons(9527);
31 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
32 int bFd = bind(sockFd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
33 if(-1 == bFd)
34 {
35 sys_err("bind error");
36 }
37 //设置监听上限
38 int lfd = listen(sockFd, 128);
39 if(-1 == lfd)
40 {
41 sys_err("listen error");
42 }
43
44 //阻塞等待连接
45 struct sockaddr_in clit_addr; // 创建一个客户的地址结构
46 socklen_t clit_addr_len = sizeof(clit_addr);
47
48 int afd= accept(sockFd, (struct sockaddr *)&clit_addr, &clit_addr_len);
49 if(-1 == afd)
50 {
51 sys_err("accept error");
52 }
53 char clientIp[1024];
54 memset(clientIp, 0, sizeof(clientIp));
55 //获取客户端IP地址
56 printf("clientIp:%s\n", inet_ntop(AF_INET, (void *)&(clit_addr.sin_addr.s_addr), clientIp, sizeof(clientIp)));
57
58 //读取客户端数据
59 char buf[BUFSIZ];
60 while(1)
61 {
62 int ret = read(bFd, buf, sizeof(buf));//返回读到的字节个数
63 if(-1 == ret)
64 {
65 sys_err("read error\n");
66 }
76 for(int i=0; i < ret; i++)
77 {
78 buf[i] = toupper(buf[i]);
79 }
80 //写入数据
81 write(STDOUT_FILENO, buf, ret);
82 int wfd = write(bFd, buf, ret);
83 if(-1 == wfd)
84 {
85 sys_err("write error\n");
86 }
87
88 }
89 close(sockFd);
90 close(afd); //关闭客户端服务端的文件描述符
91
92 return 0;
93 }
3.2 客户端代码
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <sys/socket.h>
6 #include <arpa/inet.h>
7 #include <errno.h>
8
9 int main()
10 {
11 int sockFd = socket(AF_INET, SOCK_STREAM, 0);
12 if(-1 == sockFd)
13 {
14 perror("socket error\n");
15 exit(0);
16 }
17 struct sockaddr_in serv_addr;
18 serv_addr.sin_family = AF_INET;
19 serv_addr.sin_port = htons(9527);
20 inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
21 int cfd = connect(sockFd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
22 if(-1 == cfd)
23 {
24 perror("connect error\n");
25 exit(0);
26 }
27 int loop = 5;
28 char buf[1024];
29 while(loop--)
30 {
31 int ret = read(STDIN_FILENO, buf, sizeof(buf));
32 write(sockFd, buf, ret); //写数据到服务端
33 ret = read(sockFd, buf, sizeof(buf)); //从服务端读取数据
34 write(STDOUT_FILENO, buf, ret); //数据打印
35 //sleep(1);
36 }
37 close(sockFd);
38 return 0;
39 }