C语言网络编程其实本质上也是多进程之间通过socket套接字进行通信,知识进程可能位于不同的服务器上,常用的TCP/IP协议有3种套接字类型,如下所示:
流套接字用于提供面向连接、可靠的数据传输服务,该服务保证数据能够实现无差错、无重复发送,并按照顺序接受。流套接字之所以能偶实现可靠的数据服务,原因在于使用了TCP传输控制协议。
数据包套接字提供了一种无连接的服务,该服务不能保证数据传输的可靠性,数据有可能在传输过程中丢失或者出现数据重复,且无法保证顺序的接受数据。数据报套接字使用UDP进行数据传输。
原始套接字允许对较低层次的协议直接访问,常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为器可以自如控制Window下的多种协议,能够对网络地城的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。如:通过原始套接字接受发向本机的ICMP、IGMP,或者接受TCP/IP栈不能处理的IP包。
套接字通常由三个参数构成:IP地址, 端口号、传输层协议。C语言进行套接字编程的时候,通常会使用sockaddr和sockaddr_in两种数据类型,用于保存套接字信息。
struct sockaddr { // 地址族,2字节 unsigned short sa_family; // 存放地址和端口 char sa_data[14]; } struct sockaddr_in { // 地址族 short int sin_family; // 端口号 unsigned short int sin_port; // 地址 struct in_addr sin_addr; // 8字节数组,全为0,该字节数组的作用是为了让两种数据结构大小相同而保留的空字节 unsigned char sin_zero[8]; }
对于sockaddr,大部分的情况下知识用于bind、connect、recvform、sendto等函数的参数,指明地址信息,在一般编程中,并不对此结构体直接操作,而是用sockaddr_in代替。
两种数据结构中,地址族都占2个字节,常见的地址族AF_INET, AF_INET6, AF_LOCAL。这里要注意字节序的问题,建议使用以下函数来对端口和地址进行处理。
uint16_t htons(uint16_t bost16bit) uint32_t htonl(uint32_t bost32bit) uint16_t ntons(uint16_t net16bit) uint32_t ntons(uint32_t net32bit)
客户端和服务器的连接和三次握手发生在accept函数下,listen函数知识创建了socket的监听模式。
使用socket进行TCP通信时,经常使用的函数如下表所示。
函数 | 作用 |
---|---|
socket | 用于建立一个socket连接 |
bind | 将socket与本机的一个端口绑定, 随后可以在该端口监听服务请求 |
connect | 面向连接的客户程序使用connect函数来配置socket,并于远程服务器建立一个连接 |
listen | 是socket处于被动监听模式, 并为该socket建立一个输入数据队列,将到达服务器请求保存在此队列中,直到程序处理他们 |
accept | 让服务器接收客户端的连接请求 |
close | 停止在该socket上的任何操作 |
send | 数据发送函数 |
recv | 数据接收函数 |
服务端程序流程如下:
#include <stdio.h> #include <string.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #define MAX_SIZE 512 #define PORT 3332 int main(void) { int sockfd; int sock_fd; int recvnum; int addrlen = sizeof(struct sockaddr); struct sockaddr_in my_addr; struct sockaddr addr; char buf[MAX_SIZE]; // 填充服务器端的数据,用于套接字绑定 bzero(&my_addr, sizeof(struct sockaddr_in)); my_addr.sin_family = AF_INET; // 设置为IPV4 my_addr.sin_port = htons(PORT); // 将端口号主机序转换为网络序 my_addr.sin_addr.s_addr = inet_addr("192.168.192.128"); // ip设置为192.168.192.128 // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { printf("create socket error!\n"); exit(1); } // 绑定套接字 if (bind(sockfd, (struct sockaddr *)&my_addr, addrlen) < 0) { printf("bind error!\n"); exit(1); } // 监听端口和ip,设置最大连接数为3 if (listen(sockfd, 3) < 0) { printf("listen error!\n"); exit(1); } // 建立服务器端和客户端连接 sock_fd = accept(sockfd, &addr, &addrlen); // 建立连接后,产生新的套接字 if (sock_fd < 0) { printf("accept error!\n"); exit(1); } // 接收数据 if ((recvnum = recv(sock_fd, (void *)buf, MAX_SIZE, 0)) < 0) { printf("recv error!\n"); exit(1); } buf[recvnum] = '\0'; printf("recv from client: %s\n", buf); memset(buf, 0, MAX_SIZE); // 关闭连接 close(sockfd); close(sock_fd); return 0; }
客户端程序流程如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #define MAX_SIZE 512 #define PORT 3332 int main() { int sockfd; int addrlen = sizeof(struct sockaddr); char buf[MAX_SIZE]; struct sockaddr_in serv_addr; // 填充服务器端数据 bzero(&serv_addr, sizeof(struct sockaddr_in)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = ntohs(PORT); serv_addr.sin_addr.s_addr = inet_addr("192.168.192.128"); // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { printf("create socket error!\n"); exit(1); } // 连接服务器端 if (connect(sockfd, (struct sockaddr *)&serv_addr, addrlen) < 0) { printf("connect error!\n"); exit(1); } // 发送数据到服务端 memset(buf, 0, MAX_SIZE); printf("enter some text:"); scanf("%s", buf); if (send(sockfd, (void *)buf, MAX_SIZE, 0) < 0) { printf("send error!\n"); exit(1); } // 关闭连接 close(sockfd); return 0; }