套接字是通信断点的抽象,可以看做是一种文件,正如文件使用文件描述符访问,套接字可以用套接字描述符访问,在linux系统中,套接字描述符是一种文件描述符。
在进行套接字编程之前,需要知道如何标识目标通信进程,目标通信进程由网络地址和端口标识。我们知道计算机处理器的结构不同,字节序可能不同,分为大端可小端,大端模式下高地址存储低字节,小端模式相反。在相同的计算机上通信时,不存在字节序的问题,但是如果是在不同的计算机之间通信,那么需要考虑字节序。网络协议指定了字节序称之为网络字节序,在利用socket进行通信时,应用程序可能需要在网络字节序与主机字节序之间进行转换,以确保交换格式化数据时字节序不会出现问题。TCP/IP使用大端字节序,在<arpa/inet.h>中定义了4个网络字节序转换函数如下:
#include <arpa/inet.h> uint32_t htonl(uint32_t hostint32); /*将32位的数据从主机字节序转换为网络字节序*/ uint16_t htons(uint16_t hostint16); /*将16位的数据从主机字节序转换为网络字节序*/ uint32_t ntohl(uint32_t netint32); /*将32位的数据从网络字节序转换为主机字节序*/ uint16_t ntohs(uint16_t netint16); /*将16位的数据从网络字节序转换为主机字节序*/这里h表示主机字节序,n表示网络字节序。l标识长整数,s标识短整数。
在使用bind或者connect函数之前,通常需要进行网络字节序与主机字节序的转换。一个转换例子如下:
testaddr.sin_addr.s_addr = htonl(INADDR_ANY); /*INADDR_ANY表示任意地址 0.0.0.0*/ testaddr.sin_port = hton(80); /*80是一个端口*/这些转换是整数之间的转换,有时候还需要在文本格式字符串和网络字节序的二进制地址之间进行转换,下面将会提到。
(二)地址格式
接下来,还需要知道socket编程中的地址格式。不同的通信域地址格式也有所不同,为使得不同的地址格式能够适用于相同的套接字函数,地址必须被显示地被转换成通用地址格式,在IPv4中由结构体sockaddr表示如下(注:IPv6中该结构为 stuct_in6_addr):
struct sockaddr { unsigned short sa_family; /* address falmily. eg. AF_INET */ char sa_data[]; /* address*/ . . . };在IPv4协议中,网络地址在<netinet/in.h>中定义如下:
#include<netinet/in.h> struct in_addr { in_addr_t s_addr; /*addrress*/ } struct sockaddr_in { sa_family_t sin_family; /*address family*/ in_port_t sin_port; /*port*/ struct in_addr sin_addr; /*address*/ };一个使用例子如下:
struct sockaddr serverAddr; /*Convert server IP and PORT*/ serverAddr.sin_family = AF_INET; /*ipv4*/ serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /*in_addr_t inet_addr(const char *ip)*/ serverAddr.sin_port = htons(PORT); /*uint16_t htons(uint16_t hostshort)*/ printf("server ip and port is 127.0.0.1: %d\n", PORT); /*connect*/ /* int connect (int sockfd, struct sockaddr *serv_addr, socketlen_t addrlen)*/ ret = connect(clientSocket, (struct sockaddr *)(&serverAddr), (socklen_t)(sizeof(serverAddr)) );这里用到了inet_addr,该函数可以将文本字符串(xxx.xxx.xxx.xxx)表示的地址转换为网络字节序的二进制地址,相较于常数之间转换的htonl,inet_addr函数更为常用;反过来也可以使用inet_ntoa将网络字节序的地址转换为文本字符串形式的地址,以便于打印出更容易理解的点分十进制地址格式。函数原型如下:
in_addr_t inet_addr(const char *addrStr) /*文本字符串格式的地址转换为网络字节序的地址*/ char* inet_ntoa(struct in_addr inAddr); /*网络字节序的地址的地址转换为文本字符串格式的地址*/例子:
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); ptrStr = inet_ntoa(inet_ntoa);(三)socket编程接口
(1)创建套接字 socket
linux系统下创建一个套接字描述符调用socket函数,函数原型如下:
int socket(int domain , int type , int protocol));其中domain确定通信特性,指定地址族,其值可取如下值:
AF_INET: IPv4因特网
AF_INET6: IPv6因特网
AF_UNIX: UNIX域
AF_UPSPEC:未指定
其中type为套接字类型,可取SOCK_DGRAM SOCK_RAW SOCK_SEQPACKET SOCK_STREAM等,例如SOCK_STREAM指有序、可靠、面向连接的字节流。
其中protocol通常为0,表示为指定的域和类型自动选择默认协议。
例子:
serverSocket = socket(AF_INET, SOCK_STREAM, 0);(2) 服务端绑定地址和监听端口
int bind (int sockfd , struct sockaddr *my_addr , socketlen_t addrlen);例子:
struct sockaddr_in serverAddr; memset(&serverAddr, 0x00, sizeof(serverAddr)); /*Convert IP and PORT*/ serverAddr.sin_family = AF_INET; /*ipv4*/ serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); /*uint32_t htonl(uint32_t hostlong); INADDR_ANY: 0.0.0.0*/ serverAddr.sin_port = htons(PORT); /*uint16_t htons(uint16_t hostshort)*/ /*bind*/ /*int bind (int sockfd , struct sockaddr *my_addr , socketlen_t addrlen)*/ ret = bind(serverSocket, (struct sockaddr *)(&serverAddr), (socklen_t)(sizeof(serverAddr)) );(3)服务端监听函数
int listen(int sockfd, int backlog);其中backlog是加入等待队列的未完成连接的请求数量。客户端连接请求要加入服务端socket连接请求队列,等待服务端进程调用accept函数与客户端建立连接,如果队列已满,那么系统会拒绝后续请求连接。
(4)服务accept函数
int accept(int sockfd, struct sockadde *clientAddr, socketlen_t *clientAddrLen); /*这里第三个参数是长度指针,区别于bind的第三个参数是整数变量*/其中sockfd是服务器socket,为输入参数,而clientAddr和clientAddrLen为输出参数,标识的是客户端连接,该函数返回一个用于与客户端通信的socket。如果不需要标识客户端地址信息,第二和第三个参数可以为NULL和0,比如如果不需要打印客户端连接的请求地址和端口,就可以用NULL和0。到这里我们可以看出,服务端有两个socket,一个调用socket函数得到的serverSocket用于监听处理客户端的连接请求,另一个与客户端建立连接时调用accept函数得到的对应客户端连接的clientSocket,clientSocket用于与客户端进行数据交互,连接关闭后clientSocket不可再用。
例子:
memset(&clientAddr, 0x00, sizeof(clientAddr)); addrLen = sizeof(clientAddr); clientSocket = accept(serverSocket, (struct sockaddr *)(&clientAddr), (socklen_t*)(&addrLen)); if(clientSocket == -1) { printf("accept() error: %s\n", strerror(errno)); return -1; } printf("Connected. client ip[%s]:port[%d]\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));(5)客户端连接请求
int connect (int sockfd, struct sockaddr *serv_addr, socketlen_t addrlen);其中sockfd是客户端的socket描述符,serv_addr是服务器的地址,addrlen 是sizeof(struct sockaddr)。
例子:
/*Convert server IP and PORT*/ serverAddr.sin_family = AF_INET; /*ipv4*/ serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /*in_addr_t inet_addr(const char *ip)*/ serverAddr.sin_port = htons(PORT); /*uint16_t htons(uint16_t hostshort)*/ printf("server ip and port is 127.0.0.1: %d\n", PORT); /*connect*/ /* int connect (int sockfd, struct sockaddr *serv_addr, socketlen_t addrlen)*/ ret = connect(clientSocket, (struct sockaddr *)(&serverAddr), (socklen_t)(sizeof(serverAddr)) );(6)发送数据
size_t send(int sockfd, const void *buf, size_t nbytes, int flags);flags指定了接收数据的一些方式,通常为0。
(7)接收数据
ssize_t recv( int sockfd, void *buf, size_t nbytes, int flags);flags指定了接收数据的一些方式,通常为0。需要注意的是buf会以\n结束。
(四)socket实现TCP通信
(1)client.c
/************************************** * socket test * client * created by CAO Fei * date: 2018-04-29 *************************************/ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 28111 /*port*/ #define BUF_LEN 1024 /* struct sockaddr { sa_family_t sin_family; char sa_date[]; ; : } #include<netinet/in.h> struct in_addr { in_addr_t s_addr; //IPV4 addr } struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; } 因特网地址结构sockaddr_in需要转化为通用的地址结构sockaddr; 地址和端口需要转换为网络字节序(整数转为网络字节序:htonl htons; 点分十进制文本转为二进制网络字节序:inet_addr inet_pton); */ /*client*/ int main(void) { int ret; int clientSocket; char buf[BUF_LEN]={0}; struct sockaddr_in serverAddr; struct sockaddr_in clientAddr; memset(&serverAddr, 0x00, sizeof(serverAddr)); memset(&clientAddr, 0x00, sizeof(clientAddr)); /*clientSocket*/ clientSocket = socket(AF_INET, SOCK_STREAM, 0); /*int socket(int domain , int type , int protocol)*/ if(clientSocket == -1) { printf("socket() error: %s\n", strerror(errno)); return -1; } printf("socket() success.\n"); /*Convert server IP and PORT*/ serverAddr.sin_family = AF_INET; /*ipv4*/ serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); /*in_addr_t inet_addr(const char *ip)*/ serverAddr.sin_port = htons(PORT); /*uint16_t htons(uint16_t hostshort)*/ printf("server ip and port is 127.0.0.1: %d\n", PORT); /*connect*/ /* int connect (int sockfd, struct sockaddr *serv_addr, socketlen_t addrlen)*/ ret = connect(clientSocket, (struct sockaddr *)(&serverAddr), (socklen_t)(sizeof(serverAddr)) ); if(ret == -1) { printf("connect() error: %s\n", strerror(errno)); return -1; } printf("connect() success. connected\n"); /*send*/ printf("Begin to communicate\n"); while(1) { memset(buf, 0x00, sizeof(buf)); printf("client: "); fflush(stdin); if(fgets(buf, sizeof(buf)-1, stdin) == NULL) { printf("fgets() encounters an error or EOF\n"); fflush(stdin); break; } fflush(stdin); /*size_t send(int sockfd, const void *buf, size_t nbytes, int flags);*/ /*send*/ ret = send(clientSocket, buf, sizeof(buf)-1, 0); if(ret == -1) { printf("send() error: %s\n", strerror(errno)); return -1; } if(memcmp(buf, "exit", 4) == 0) { /*stop*/ printf("close connection.\n"); close(clientSocket); break; } /*recv*/ memset(buf, 0x00, sizeof(buf)); if(recv(clientSocket, buf, sizeof(buf)-1, 0) <0 ) /*ssize_t recv( int sockfd, void *buf, size_t nbytes, int flags)*/ { printf("recv() error: %s\n", strerror(errno)); close(clientSocket); break; } printf("server: %s", buf); if(memcmp(buf, "exit", 4) == 0 || memcmp(buf, "kill", 4) == 0) { /*stop*/ printf("close connection.\n"); close(clientSocket); break; } } return 0; }
(2)server.c
/************************************** * socket test * server * created by CAO Fei * date: 2018-04-29 *************************************/ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 28111 /*port*/ #define BUF_LEN 1024 /* struct sockaddr { sa_family_t sin_family; char sa_date[]; ; : } #include<netinet/in.h> struct in_addr { in_addr_t s_addr; //IPV4 addr } struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; } 因特网地址结构sockaddr_in需要转化为通用的地址结构sockaddr; 地址和端口需要转换为网络字节序(整数转为网络字节序:htonl htons; 点分十进制文本转为二进制网络字节序:inet_addr inet_pton); */ /*server*/ int main(void) { int ret; int serverSocket; int clientSocket; int addrLen; char buf[BUF_LEN]={0}; struct sockaddr_in serverAddr; struct sockaddr_in clientAddr; memset(&serverAddr, 0x00, sizeof(serverAddr)); memset(&clientAddr, 0x00, sizeof(clientAddr)); /*serverSocket*/ serverSocket = socket(AF_INET, SOCK_STREAM, 0); /*int socket(int domain , int type , int protocol)*/ if(serverSocket == -1) { printf("socket() error: %s\n", strerror(errno)); return -1; } printf("socket() success.\n"); /*Convert IP and PORT*/ serverAddr.sin_family = AF_INET; /*ipv4*/ serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); /*uint32_t htonl(uint32_t hostlong); INADDR_ANY: 0.0.0.0*/ serverAddr.sin_port = htons(PORT); /*uint16_t htons(uint16_t hostshort)*/ /*bind*/ /*int bind (int sockfd , struct sockaddr *my_addr , socketlen_t addrlen)*/ ret = bind(serverSocket, (struct sockaddr *)(&serverAddr), (socklen_t)(sizeof(serverAddr)) ); if(ret == -1) { printf("bind() error: %s\n", strerror(errno)); return -1; } printf("server bind port:[%d]\n", PORT); /*listen*/ ret = listen(serverSocket, 128); /*int listen(int sockfd, int backlog) */ if(ret == -1) { printf("listen() error: %s\n", strerror(errno)); return -1; } /*accept and recv*/ printf("Begin to accept\n"); while(1) { /*accept*/ /*int accept(int sockfd, void *addr, socketlen_t *addrlen); here addr is clientAddr*/ /*如果不关心客户端标识,可以bind(sockfd, NULL, 0)来调用; 否则my_addr是客户地址指针,而*addrlen要设置成一个能够存储地址长度的缓冲区长度,my_addr存储长度不超过*addrlen的地址数据 如果实际长度不足,会被改更改长度 */ memset(&clientAddr, 0x00, sizeof(clientAddr)); addrLen = sizeof(clientAddr); clientSocket = accept(serverSocket, (struct sockaddr *)(&clientAddr), (socklen_t*)(&addrLen)); if(clientSocket == -1) { printf("accept() error: %s\n", strerror(errno)); return -1; } printf("Connected. client ip[%s]:port[%d]\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); printf("Begin to communicate\n"); while(1) { /*recv*/ memset(buf, 0x00, sizeof(buf)); if(recv(clientSocket, buf, sizeof(buf)-1, 0) <0 ) /*ssize_t recv( int sockfd, void *buf, size_t nbytes, int flags)*/ { printf("recv() error: %s\n", strerror(errno)); close(clientSocket); break; } printf("client: %s", buf); if(memcmp(buf, "exit", 4) == 0) { close(clientSocket); printf("close clientSocket\n"); break; } /*send*/ fflush(stdin); memset(buf, 0x00, sizeof(buf)); printf("server: "); if(fgets(buf, sizeof(buf)-1, stdin) == NULL) { printf("fgets() encounters an error or EOF\n"); close(clientSocket); fflush(stdin); break; } fflush(stdin); send(clientSocket, buf, sizeof(buf)-1, 0); if(memcmp(buf, "exit", 4) == 0 || memcmp(buf, "kill", 4)==0) { close(clientSocket); printf("close clientSocket\n"); break; } } if(memcmp(buf, "kill", 4) == 0) { close(serverSocket); printf("close serverSocket\n"); break; } } return 0; }
(3)makefile
CC = gcc CFLAG = -g -c -Wall all: server client %.o:%.c $(CC) $(CFLAG) -c $*.c server:server.o $(CC) -o $@ $< client:client.o $(CC) -o $@ $< clean: rm -f *.o
测试结果如下:
需要注意的是,server进程需要先启动,否则client进程先启动则会报错。客户端或者服务端输入exit则结束一次TCP通信,此时服务端仍旧正常监听,如果服务端输入kill,则结束服务端进程。
代码可见我的github