杭州网站制作工具,教学方面网站建设,西安十强广告公司名单,政务网站系统C网络编程(一) socket通信
前言
本次内容简单描述C网络通信中#xff0c;采用socket连接客户端与服务器端的方法#xff0c;以及过程中所涉及的函数概要与部分函数使用细节。记录本人C网络学习的过程。
网络通信的Socket
socket,即“插座”,在网络中译作中文“套接字”,应…C网络编程(一) socket通信
前言
本次内容简单描述C网络通信中采用socket连接客户端与服务器端的方法以及过程中所涉及的函数概要与部分函数使用细节。记录本人C网络学习的过程。
网络通信的Socket
socket,即“插座”,在网络中译作中文“套接字”,应用于计算机网络建立起数据连接。基于TCP/IP协议进行通信将本地的ip地址与端口号相结合。两个程序可以通过各自的socket建立一个通道用于数据传输。
socket提供多种机制以供选择但常用的一般为两种即SOCK_STREAM(流)和SOCK_DGRAM(数据报)。
流socket基于TCP协议其建立的通道是一个具有有序、可靠、全双工的字节流。
数据报socket基于UDP协议其建立的通道传送数据是不可靠的尽最大努力交付的但传输速度较快。
目前在网络中使用的数据传送基本是基于TCP协议。
网络通信基本过程 首先建立起服务端在服务端中采用socket()构建方法建立服务端响应socket其主要作用是用于接收由客户端socket发起的建立请求。随后bind()将创建的响应socket与本地的ip地址及设置的端口绑定。listen()为响应socket设置被动监听状态监听客户端发来的建立请求。accept()代表一切就绪此时服务器程序运行进入阻塞状态等待客户端程序发来请求。
客户端中同样需要先建立起客户端socket该socket所采用的协议应当与服务器端socket相同。客户端socket不需要绑定本地ip与端口但应当指明所连接的服务器端的ip地址与端口号通过connet()主动发起连接请求与服务器端socket相连。
服务器端accept()程序收到客户端socket发来的请求后会立即创建一个新的服务socket与客户端的socket相对应用于数据的收发与传送。即服务器端会有两个socket而客户端仅有一个socket。
当双方数据交互结束后各自调用close()释放掉本地的socket。
代码示例
//server.cpp
#includestdio.h
#includestring.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h
#includeunistd.h
#includecerrno#define MY_PORT 1234//端口号
#define BUF_SIZE 1024//最大缓存
#define QUEUE 20//最大连接数int main(){int server_sockfd socket(AF_INET,SOCK_STREAM,0);//建立响应socketstruct sockaddr_in server_sockaddr;//保存本地地址信息server_sockaddr.sin_family AF_INET;//采用ipv4server_sockaddr.sin_port htons(MY_PORT);//指定端口server_sockaddr.sin_addr.s_addr htonl(INADDR_ANY);//获取主机接收的所有响应if(bind(server_sockfd,(struct sockaddr *)server_sockaddr,sizeof(server_sockaddr))-1){//绑定本地ip与端口perror(Bind Failure\n);printf(Error: %s\n,strerror(errno));//输出错误信息return -1;}printf(Listen Port : %d\n,MY_PORT);if(listen(server_sockfd,QUEUE) -1){//设置监听状态perror(Listen Error);return -1;}char buffer[BUF_SIZE];//一次传输的数据缓存struct sockaddr_in client_addr;//保存客户端地址信息socklen_t length sizeof(client_addr);//需要的内存大小printf(Waiting for connection!\n);int connect_fd accept(server_sockfd,(struct sockaddr *)client_addr,length);//等待连接返回服务器端建立连接的socketif(connect_fd -1){//连接失败perror(Connect Error);return -1;}printf(Connection Successful\n);while(1){//数据收发与传输memset(buffer,0,sizeof(buffer));int len recv(connect_fd,buffer,sizeof(buffer),0);//接收数据// input exit or runtime errorif(strcmp(buffer,exit\n)0 ||len 0) break;printf(client send message: %s,buffer);strcpy(buffer,successful);send(connect_fd,buffer,strlen(buffer),0);//发送数据printf(send message: %s\n,buffer);}close(connect_fd);//关闭数据socketclose(server_sockfd);//关闭响应socketreturn 0;
}//client.cpp
#includestdio.h
#includestring.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h
#includeunistd.h
#includecerrno#define MY_PORT 1234
#define BUF_SIZE 1024
#define SERVER_IP 127.0.0.1//服务端ip,这里是本机int main(){int client_sockfd socket(AF_INET,SOCK_STREAM,0);//建立客户端socketstruct sockaddr_in servaddr;//保存服务器端地址信息memset(servaddr,0,sizeof(servaddr));servaddr.sin_family AF_INET;//ipv4协议servaddr.sin_port htons(MY_PORT);//端口servaddr.sin_addr.s_addr inet_addr(SERVER_IP);//ip地址printf(connect to %s:%d\n,SERVER_IP,MY_PORT);int connect_fd connect(client_sockfd,(struct sockaddr *)servaddr,sizeof(servaddr));//建立连接if(connect_fd0){perror(Connect Error);_exit(1);}printf(Connect Successful\n);char sendbuf[BUF_SIZE];char recvbuf[BUF_SIZE];while(fgets(sendbuf,sizeof(sendbuf),stdin) ! NULL){//数据传送memset(recvbuf,0,sizeof(recvbuf));printf(send message:%s,sendbuf);send(client_sockfd,sendbuf,strlen(sendbuf),0);if(strcmp(sendbuf,exit\n)0) break;int len recv(client_sockfd,recvbuf,sizeof(recvbuf),0);if(len0){printf(receive failure);break;}printf(recv message:%s\n,recvbuf);memset(sendbuf,0,sizeof(sendbuf));}close(client_sockfd);//关闭客户端socketreturn 0;
}函数分析
主机字节序和网络字节序
主机字节序即在计算机内部存储数据的格式通常为小端序即低地址存放低字节的内容比如说一个ipv4的地址当其作为数字存储时一共需要四个字节来保存值对于127.0.0.1来说主机字节序中最低位的字节保存了127最高位则是1。
网络字节序是TCP/IP中规定好的一种数据格式采用大端序即高地址存放低字节的内容同样对于127.0.0.1来说网络字节序中最低位的字节保存了1最高位则是127。
字节地址0123主机字节序01111111000000000000000000000001网络字节序00000001000000000000000001111111
#include netinet/in.h
unsigned long int htonl(unsigned long int hostlong);//主机转网络
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);//网络转主机
unsigned short int ntohs(unsigned short int netshort);htonl表示host to network long依次类推
INADDR_ANY其值为0.0.0.0其意义便是不监听特定的ip,而接收所有通过指定端口发送至本机的信号。
Socket地址
通用地址
#include bits/socket.h//sys/socket.h头文件包含了此头
struct sockaddr
{sa_family_t sa_family; //地址族类型char sa_data[14]; //地址值
}专用地址
TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体它们分别用于IPv4和IPv6此处仅列述IPv4。
#include netinet/in.h
struct sockaddr_in
{sa_family_t sin_family;/*地址族AF_INET*/u_int16_t sin_port;/*端口号要用网络字节序表示*/struct in_addr sin_addr;/*IPv4地址结构体见下面*/unsigned char sin_zero[8];/*为了保持与struct sockaddr长度相同*/
};struct in_addr
{u_int32_t s_addr;/*IPv4地址要用网络字节序表示*/
};在实际使用时sockaddr_in需要转化为通用socket地址类型sockaddr强制转换即可因为所有socket编程接口使用的地址参数的类型都是sockaddr。
IP地址转换
#include arpa/inet.h
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp,struct in_addr* inp);
char* inet_ntoa(struct in_addr in);inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回INADDR_NONE。
inet_aton函数完成和inet_addr同样的功能但是将转化结果存储于参数inp指向的地址结构中。它成功时返回1失败则返回0。
inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。但需要注意的是该函数内部用一个静态变量存储转化结果函数的返回值指向该静态内存因此inet_ntoa是不可重入的。
注以上函数仅用于IPv4的地址转换。
创建socket
#include sys/types.h
#include sys/socket.h
int socket(int domain,int type,int protocol);
/*
domain参数:告诉系统使用哪个底层协议族
type参数:指定服务类型
protocol参数:在前两个参数构成的协议集合下再选择一个具体的协议
*/参数通常取值
domainPF_INETProtocol Family of Internet用于IPv4、PF_INET6用于IPv6
typeSOCK_STREAM(流TCP)、SOCK_DGRAM(数据报UDP)
protocol0(默认协议)
当socket系统调用成功时返回一个socket文件描述符失败则返回-1并设置errno。
绑定IP地址和端口
#include sys/types.h
#include sys/socket.h
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符addrlen参数指出该socket地址的长度。
bind成功时返回0失败则返回-1并设置errno。其中两种常见的errno是EACCES和EADDRINUSE它们的含义分别是
EACCES被绑定的地址是受保护的地址仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口端口号为01023上时bind将返回EACCES错误。EADDRINUSE被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址。
监听socket
#include sys/socket.h
int listen(int sockfd, int backlog);
/*
sockfd: 指定被监听的socket
backlog: 设置内核监听队列的最大长度
*/监听队列:即处于向服务端socket发出连接请求但TCP建立三次握手中尚未进行第二次握手的序列。
监听队列的长度如果超过backlog服务器将不受理新的客户连接客户端也将收到ECONNREFUSED错误信息。
listen成功时返回0失败则返回-1并设置errno。
注listen()函数执行成功后客户端发来连接请求在经过listen后TCP的三次握手已经完成建立了连接。
接收连接
#include sys/types.h
#include sys/socket.h
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
/*
sockfd:是执行过listen系统调用的监听socket
addr:用来获取被接受连接的远端socket地址
addrlen:指出远程socket的长度
返回值:一个新的连接socket
*/函数调用成功返回的socket唯一地标识了被接受的这个连接服务器可通过读写该socket来与被接受连接对应的客户端通信。
调用失败则返回-1并设置errno。
发起连接
#include sys/types.h
#include sys/socket.h
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);sockfd参数由socket系统调用返回一个socket。serv_addr参数是服务器监听的socket地址addrlen参数则指定这个地址的长度。
connect成功时返回0。一旦成功建立连接sockfd就唯一地标识了这个连接客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的errno是ECONNREFUSED和ETIMEDOUT它们的含义如下
ECONNREFUSED目标端口不存在连接被拒绝。ETIMEDOUT连接超时。
关闭连接
#include unistd.h
int close(int fd);//fd是待关闭的socketclose系统调用并非总是立即关闭一个连接而是将fd的引用计数减1。只有当fd的引用计数为0时才真正关闭连接。类似于Linux系统中的硬链接数。比方说在多进程程序中父进程和子进程都对一个socket操作只有在所有进程中都调用close()关闭该socket该socket才会被关闭。
有一个函数可以立即终止连接
#include sys/socket.h
int shutdown(int sockfd,int howto);sockfd参数是待关闭的socket。howto参数决定了shutdown的行为可取值如下
SHUT_RD 关闭读SHUT_WR 关闭写SHUT_RDWR 关闭读写
数据读写
#include sys/types.h
#include sys/socket.h
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
ssize_t send(int sockfd, const void* buf, size_t len, int flags);此处函数仅针对TCP流数据读写对UDP数据报的读写暂且不表。
recv读取sockfd上的数据buf和len参数分别指定读缓冲区的位置和大小flags通常设置为0即可。recv成功时返回实际读取到的数据的长度它可能小于我们期望的长度len。因此我们可能要多次调用recv才能读取到完整的数据。recv可能返回0这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno。
send往sockfd上写入数据buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度失败则返回-1并设置errno。
网络信息API
#include netdb.h
struct hostent*gethostbyname(const char* name);//name:指定目标主机的主机名
struct hostent*gethostbyaddr(const void* addr, size_t len, int type);
/*
addr指定目标主机的IP地址
len指定addr所指IP地址的长度
type指定addr所指IP地址的类型(AF_INTE、AF_INET6)
*/struct hostent
{char* h_name;/*主机名*/char** h_aliases;/*主机别名列表可能有多个*/int h_addrtype;/*地址类型地址族*/int h_length;/*地址长度*/char** h_addr_list;/*按网络字节序列出的主机IP地址列表数字形式不是点分十进制*/
};gethostbyname函数根据主机名称获取主机的完整信息gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查找主机如果没有找到再去访问DNS服务器。
//示例
struct hostent *res gethostbyname(www.baidu.com);
if (res)
{for (int i 0; res-h_addr_list[i]; i)printf(IP addr %d: %s\n, i 1, inet_ntoa(*(struct in_addr *)res-h_addr_list[i]));
}