一. 网络套接字:
套接字就是socket,socket就像网络两端的两个站点,他们之间的要走的路就是数据传输的线路。套接字会标识进程的ip和端口号,其实就标识了主机和里面那个进程(程序)。网络套接字是一个文件,用文件描述符表示。
在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
二. 数据转化
1. 网络字节序
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678大端法:(网络存储) 高位存低地址。地位存高地址。所以我们要将网络本地字节序转换为网络字节序。
常见的函数有:
1.字节序转换函数:
(1)htonl()
定义:
#include <winsock.h>
u_short PASCAL FAR htons( u_short hostshort);
功能:将本地字节序转化为网络字节序
参数:16位数从主机字节顺
返回值:网络字节顺序的值
(2)htons --> 本地--》网络 (port)
(3)ntohl --> 网络--》 本地(IP)
(4)ntohs --> 网络--》 本地(Port)
三个函数类似
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
2.IP地址转化函数:
inet_pton()函数:
定义:
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
作用:将点分十进制的IP地址转化为网络字节序的IP地址
参数:
af:AF_INET、AF_INET6Z(那种协议ipv6-ipv4)
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
inet_ntop()函数
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);网络字节序 ---> 本地字节序(string IP)
参数:
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值:
成功:dst。
失败:NULL
三. sockaddr数据结构
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,然后函数内部再强制类型转化为所需的地址类型。注意bind等函数使用的时第一种结构体,使用时要强制转换。两种结构体的命名如下:
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family地址结构类型 ipv4和ipv6 */
__be16 sin_port; /* Port number 端口号*/
struct in_addr sin_addr;/* Internet addressIP地址 */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
例子:初始化一个sockaddr地址结构体
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6 //指定ipv4-ipv6
addr.sin_port = htons(9527); //端口
int dst;
inet_pton(AF_INET, "192.157.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
四. socket模型与常用函数
服务器端:建立socket,绑定端口和ip,设置监听上限,阻塞等待客户连接,连接成功返回一个新的能与客户端连接的socket,注意这个套接字不是原来的套接字。原来的套接字只用来监听。
客户端:建立socket,建立连接。相互建立连接后开始进行数据传输。重要的函数介绍如下:
(1)socket函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
作用:创建一个套接字
参数:domain:AF_INET、AF_INET6、AF_UNIX//协议类型
type:SOCK_STREAM、SOCK_DGRAM //SOCK_STREAM可靠,基于字节流如tcp;不可靠,无连接如udp
protocol: 0 //使用上一个参数最常用的默认协议tcp/udp
返回值:
成功: 新套接字所对应文件描述符
失败: -1 errno
(2)bind函数
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:给socket绑定一个地址结构 (一般是自己的IP+port)
参数:sockfd: 上面socket 函数返回值
const struct sockaddr *addr:就是一个套接字的数据结构
实例:
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr: 传入参数(struct sockaddr *)&addr //本地的端口和ip
addrlen: sizeof(addr) //地址结构的大小。
返回值:
成功:0
失败:-1 errno
设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为8888。
(3)listen函数
int listen(int sockfd, int backlog);
作用:设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
参数:sockfd: 上面的socket 函数返回值
backlog:上限数值。最大值 128.
返回值:
成功:0
失败:-1 errno
(4)accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
作用:阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
参数:sockfd: socket 函数返回值
addr:传出参数。成功与服务器建立连接的那个客户端的地址结构( IP+port)
socklen_t clit_addr_len = sizeof(addr);
addrlen:传入传出。 &clit_addr_len
入:addr的大小。
出:客户端addr实际大小。
返回值:
成功:能与客户端进行数据通信的 socket 对应的文件描述。
失败: -1 , errno
(5)connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用:使用现有的 socket 与服务器建立连接
参数:sockfd: socket 函数返回值
struct sockaddr_in srv_addr; 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
addr:传入参数。服务器的地址结构
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1 errno
代码示例:客户和服务器端实现大小写转换:
serve.c
1. #include <stdio.h>
2. #include <ctype.h>
3. #include <sys/socket.h>
4. #include <arpa/inet.h>
5. #include <stdlib.h>
6. #include <string.h>
7. #include <unistd.h>
8. #include <errno.h>
9. #include <pthread.h>
10.
11. #define SERV_PORT 9527
12.
13.
14. void sys_err(const char *str)
15. {
16. perror(str);
17. exit(1);
18. }
19.
20. int main(int argc, char *argv[])
21. {
22. int lfd = 0, cfd = 0;
23. int ret, i;
24. char buf[BUFSIZ], client_IP[1024];
25.
26. struct sockaddr_in serv_addr, clit_addr; // 定义服务器地址结构 和 客户端地址结构
27. socklen_t clit_addr_len; // 客户端地址结构大小
28.
29. serv_addr.sin_family = AF_INET; // IPv4
30. serv_addr.sin_port = htons(SERV_PORT); // 转为网络字节序的 端口号
31. serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 获取本机任意有效IP
32.
33. lfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个 socket
34. if (lfd == -1) {
35. sys_err("socket error");
36. }
37.
38. bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));//给服务器socket绑定地址结构(IP+port)
39.
40. listen(lfd, 128); // 设置监听上限
41.
42. clit_addr_len = sizeof(clit_addr); // 获取客户端地址结构大小
43.
44. cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len); // 阻塞等待客户端连接请求
45. if (cfd == -1)
46. sys_err("accept error");
47.
48. printf("client ip:%s port:%d\n",
49. inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),
50. ntohs(clit_addr.sin_port)); // 根据accept传出参数,获取客户端 ip 和 port
51.
52. while (1) {
53. ret = read(cfd, buf, sizeof(buf)); // 读客户端数据
54. write(STDOUT_FILENO, buf, ret); // 写到屏幕查看
55.
56. for (i = 0; i < ret; i++) // 小写 -- 大写
57. buf[i] = toupper(buf[i]);
58.
59. write(cfd, buf, ret); // 将大写,写回给客户端。
60. }
61.
62. close(lfd);
63. close(cfd);
64.
65. return 0;
66. }
服务器端编译测试:
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);
accept函数中的clit_addr传出的就是客户端地址结构,IP+port
于是,在代码中增加此段代码,可获取客户端信息:
printf("client ip:%s port:%d\n", inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)), ntohs(clit_addr.sin_port));
client.c
1. #include <stdio.h>
2. #include <sys/socket.h>
3. #include <arpa/inet.h>
4. #include <stdlib.h>
5. #include <string.h>
6. #include <unistd.h>
7. #include <errno.h>
8. #include <pthread.h>
9.
10. #define SERV_PORT 9527
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. int cfd;
21. int conter = 10;
22. char buf[BUFSIZ];
23.
24. struct sockaddr_in serv_addr; //服务器地址结构
25.
26. serv_addr.sin_family = AF_INET;
27. serv_addr.sin_port = htons(SERV_PORT);
28. //inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
29. inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
30.
31. cfd = socket(AF_INET, SOCK_STREAM, 0);
32. if (cfd == -1)
33. sys_err("socket error");
34.
35. int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
36. if (ret != 0)
37. sys_err("connect err");
38.
39. while (--conter) {
40. write(cfd, "hello\n", 6);
41. ret = read(cfd, buf, sizeof(buf));
42. write(STDOUT_FILENO, buf, ret);
43. sleep(1);
44. }
45.
46. close(cfd);
47.
48. return 0;
49. }
编译运行,结果如下:
这里遇到过一个问题,如果之前运行server,用Ctrl+z终止进程,ps aux列表里会有服务器进程残留,这个会影响当前服务器。解决方法是kill掉这些服务器进程。不然端口被占用,当前运行的服务器进程接收不到东西,没有回显。