目录
下面是网络socket通信的基本流程:
socket客户端代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdlib.h>
#include <getopt.h>
#define MSG_NUM 1024
#define MSG_STR "This is a network socket program"
void print_usage(char *progname)
{
printf("%s usage: \n", progname);
printf("-i(--SERVER_IP): sepcify server IP address\n");
printf("-p(--SERVER_PORT): sepcify server port.\n");
printf("-h(--Help): print this help information.\n");
return ;
}
int main(int argc,char **argv)
{
int sockfd = -1;
int rv =-1;
char *SERVER_IP = NULL;
int SERVER_PORT = 0;
int rw;
char buf[MSG_NUM];
struct sockaddr_in servaddr;
struct option longopts[] = {
{"help", no_argument, NULL, 'h'},
{"SERVER IP", required_argument, NULL, 'P'},
{"SERVER PORT", required_argument, NULL, 'P'},
{0, 0, 0, 0}
};
while( (rw=getopt_long(argc, argv, "i:p:h", longopts, NULL)) != -1 )
{
switch(rw)
{
case 'i':
SERVER_IP=optarg;
break;
case 'p':
SERVER_PORT=atoi(optarg);
break;
case 'h':
print_usage(argv[0]);
return 0;
}
}
if( !SERVER_IP || !SERVER_PORT )
{
print_usage(argv[0]);
return 0;
}
sockfd=socket(AF_INET, SOCK_STREAM,0);
if(sockfd<0)
{
printf("Create socket failure:%s\n",strerror(errno));
return 0;
}//创建一个socket描述字,它唯一标识一个socket。存放在sockfd中,后续操作都有用到。
printf("Create socket[%d] successfully!\n",sockfd);
memset(&servaddr, 0, sizeof(servaddr)); ;
servaddr.sin_family=AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
inet_aton(SERVER_IP, &servaddr.sin_addr);
rv=connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
if(rv<0)
{
printf("To socket[%s:%d] failure:%s\n ",SERVER_IP,SERVER_PORT,strerror(errno));
return -1;
goto cleanup;
}
printf("To connect socket[%d] successfully!",sockfd);
rv=write(sockfd, MSG_STR, sizeof(MSG_STR));
if(rv<0)
{
printf("Write data to socket[sockfd] failure:%s\n",strerror(errno));
return -2;
goto cleanup;
}
printf("Write data to socket[%d] successfully!\n",sockfd);
while(1)
{
memset(buf,0,sizeof(buf));
rv=read(sockfd,buf,sizeof(buf));
if(rv<0)
{
printf("Read data from socket[%d] failure:%s\n",sockfd,strerror(errno));
return -3;
goto cleanup;
}
else if (rv==0)
{
printf("The socket[%d] has disconnected!",sockfd);
return -4;
goto cleanup;
}
printf("Read %d bytes data from socket[%d]:%s\n",rv,sockfd,buf);
}
cleanup:
close(sockfd);
return 0;
}
socket操作API函数
socket就提供了各种操作对应的函数接口,下面介绍几个上面代码中用到的参数。
————————————————————————————————————————————
socket()函数
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
◆ domain:也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
◆ type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等)。
◆ protocol:protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。当protocol为0时,会自动选择type类型对应的默认协议。
————————————————————————————————————————————
connect()函数
TCP客户端程序调用socket()创建socket fd之后,就可以调用connect()函数来连接服务器。如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户的TCP连接,通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
◆ sockfd: 客户端的socket()创建的描述字
◆ addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息
◆ addrlen: socket地址的长度
在调用connect之前,我们需要先设置服务器端的IP地址和端口等信息到addr中去。
memset(&servaddr, 0, sizeof(servaddr)); ;
servaddr.sin_family=AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
inet_aton(SERVER_IP, &servaddr.sin_addr);
rv=connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
if(rv<0)
{
printf("To socket[%s:%d] failure:%s\n ",SERVER_IP,SERVER_PORT,strerror(errno));
return -1;
goto cleanup;
}
printf("To connect socket[%d] successfully!",sockfd);
————————————————————————————————————————————
read()、write()等函数
客户端在connect()连接服务器,并且服务器通过accept()建立起这个TCP socket链接之后,就可以调用网络I/O函数进行读写操作了,即实现了网络通信。具体用法可查百度或参考我上一篇博客:https://blog.csdn.net/weixin_46378291/article/details/104771835
————————————————————————————————————————————
close()、shutdown()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用close()来关闭一样。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
int close(int fd);
如果对socket fd调用close()则会触发该TCP连接断开的四路握手,有些时候我们需要数据发送出去并到达对方之后才能关闭socket套接字,则可以调用shutdown()函数来半关闭套接字:
int shutdown(int sockfd, int how);
如果how的值为 SHUT_RD 则该套接字不可再读入数据了; 如果how的值为 SHUT_WR 则该套接字不可再发送数据了; 如果how的值为 SHUT_RDWR 则该套接字既不可以读,也不可以写数据了。
————————————————————————————————————————————
inet_pton()和inet_ntop()函数
在此前的代码中我们使用的inet_aton()或inet_ntoa()函数完成IPv4点分十进制字符串和32位整形数据之间的互相转换。
inet_aton(SERVER_IP, &servaddr.sin_addr);
inet_aton() 转换网络主机地址ip(如192.168.1.10)为二进制数值,并存储在struct in_addr结构中,即第二个参数*inp,函数返回非0表示cp主机有地有效,返回0表示主机地址无效。但这两个函数只适合于IPv4的地址。下面这两个函数可以同时兼容IPv4和IPv6的地址:
int inet_pton(int family, const char *strptr, void *addrptr);
//将点分十进制的ip地址转化为用于网络传输的数值格式,返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
//将数值格式转化为点分十进制的ip地址格式,返回值:若成功则为指向结构的指针,若出错则为NULL
————————————————————————————————————————————
网络字节序和主机字节序
主机字节序:就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。
所以,在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。函数 htons() 和 htolnl() 分别用来将 端口和IP地址转换成网络字节序。参考上面代码示例。
servaddr.sin_port = htons(SERVER_PORT);
serv_addr.sin_addr.s_addr = htonl(192.168.1.141);
这里通过调用两个函数 htons() 和 htolnl() 分别用来将 端口和IP地址转换成网络字节序,这两个函数名中的 h表示host, n表 示network, s表示short(2字节/16位), l表示long(4字节/32位)。因为端口号是16位的,所以我们用htons()把端口号从主机字节序转换成网络字节序, 而IP地址是32位的,所以我们用htonl()函数把IP地址从主机字节序转换成网络字节序。