套接字编程
- UDP协议通信:
- TCP通信:
套接字编程:如何编写一个网络通信程序
1.网络通信的数据中都会包含一个完整的五元组:
sip,sport,dip,dport,protocol(源IP,源端口,对端IP,对端端口,协议)
五元组完整的描述了数据从哪来,到哪去,用什么数据格式
2.网络通信–两个主机进程之间的通信:客户端&服务端
客户端:用户使用,发起请求
服务端:网络应用提供商提供服务的程序(后台开发)
UDP协议通信:
服务端一方要提前启动,保证有数据到来时,一定能够接收;并且服务端永远都是先接收数据,因为服务端这一方并没有保存客户端的地址,没有地址绑定,也就没有数据来源,也不知道数据要发送什么,要发给谁
客户端:创建套接字,端口绑定(不推荐),发送数据,接收数据,关闭套接字
操作接口:
socket–创建套接字
domain—地址域类型(域间通信、IPv4通信、IPv6通信)AF_INET—IPv4网络协议
type–套接字类型
SOCK_STREAM ; SOCK_DGRAM
bind–为套接字绑定地址信息
int bind(int sockfd, struct sockaddr *addr, socklen_t address_len)
三个参数:套接字描述符,要绑定的地址(不同地址域,有不同的地址结构),sockaddr结构体的长度
sendto:发送数据
参数:sockfd—返回的套接字描述符
buf–要发送的数据空间起始地址
len–要发送的数据长度(从buf地址开始,发送len长度的数据)
flags–默认0-阻塞发送(发送缓冲区满了就等着)
dest_addr–对端地址信息,数据要发送给谁,目的地
addrlen–对端地址信息长度
recvfrom:接收数据
对于recvfrom的src_addr参数,是为了获取数据是谁发送的,相当于用指针接收返回值,addrlen也一样(可以看到这里的addrlen是指针)
int close(int fd)
关闭套接字,释放资源
字节序相关接口:
htonl(32位),htons(16位),ntohl,ntohs ; l–32位 ; s–16位
这几个接口已经进行了主机字节序的判断,因此无需担心自己的主机字节序
32位数据转换接口与16位不能混用,会出现数据截断,就像汉字用两个字节表示,而如果按字节输出就会乱码,并且再大小端转换时,数据截断会造成数据的错误
sockaddr结构体
udp_srv.c
#include #include #include #include #include //字节序转换接口头文件#include //地址结构类型以及协议类型宏头文件#include //套接字接口头文件int main(int agrc, char *argv[]){ if (agrc != 3) { printf("/unp_srv 192.168.2.2 9000\n"); return -1; } uint16_t port = atoi(argv[2]); // 输入的参数都是按字符串存的,这里将端口port转int char *ip = argv[1]; // 创建套接字 int socket(int domain, int type ,int protocol) int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // ipv4域,数据报,udp协议 if (socket < 0) { perror("socket error"); return -1; } // 为套接字绑定地址信息 int bind(int sockfd, struct sockaddr* addr,socklen_t len) struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); // 16位,存储用16位解析也用16位 addr.sin_addr.s_addr = inet_addr(ip); socklen_t len = sizeof(struct sockaddr_in); int ret = bind(sockfd, (struct sockaddr *)&addr, len); if (ret == -1) { perror("bind error"); return -1; } // 循环接收发送数据 while (1) { char buf[1024] = {0}; struct sockaddr_in peer; // 地址由系统设置,数据谁发的,设置的就是谁 socklen_t len = sizeof(struct sockaddr_in); ssize_t ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr *)&peer, &len); if (ret < 0) { perror("recvfrom error"); return -1; } // const char* inet_ntoa(struct in_addr addr); char *peerip = inet_ntoa(peer.sin_addr); // 转成字符串 uint16_t peerport = ntohs(peer.sin_port); printf("client[%s:%d] say: %s\n", peerip, peerport, buf); // 发送数据 // ssize_t sendto(int sockfd, void* buf, int len, int flag, struct sockaddr* peer, socklen_t len) char data[1024] = {0}; printf("server say:"); fflush(stdout); scanf("%s", data); ret = sendto(sockfd, data, strlen(data), 0, (struct sockaddr *)&peer, len); if (ret < 0) { perror("sendto error"); return -1; } } // 关闭套接字 close(sockfd); return 0;}
g++ -std=c++11 -o udp_cli udp_cli.cpp
udp_cli.cpp
#include "udp_socket.hpp"int main(int argc, char *argv[]){ if (argc != 3) { std::cout << "usage: ./udp_cli 192.168.2.2 9000\n"; return -1; } std::string srv_ip = argv[1]; uint16_t srv_port = std::stoi(argv[2]); UdpSocket cli_sock; assert(cli_sock.Socket()==true); while (1) { std::string data; std::cout << "clinet say: "; fflush(stdout); std::cin >> data; assert(cli_sock.Send(data, srv_ip, srv_port)==true); data.clear(); assert(cli_sock.Recv(&data)==true); std::cout << "server say:" << data << std::endl; } cli_sock.Close(); return 0;}
udp_socket.hpp //封装socket接口
#include #include #include #include #include #include #include #include class UdpSocket{private: int _sockfd;public: UdpSocket():_sockfd(-1){} ~UdpSocket(){Close();} bool Socket() { _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (_sockfd < 0) { perror("socket error"); return false; } return true; } bool Bind(const std::string &ip, uint16_t port) { struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(ip.c_str()); socklen_t len = sizeof(struct sockaddr_in); int ret = bind(_sockfd, (struct sockaddr *)&addr, len); if (ret < 0) { perror("bind error"); return false; } return true; } bool Recv(std::string *body, std::string *peer_ip = NULL, uint16_t *peer_port = NULL) { // ssize_t recvfrom(int _fd, void *_restrict_ _buf, size_t _n, int _flags, sockaddr *_restrict_ _addr, socklen_t *_restrict_ _addr_len) struct sockaddr_in peer; socklen_t len = sizeof(struct sockaddr_in); char tmp[4096] = {0}; ssize_t ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr *)&peer, &len); if (ret < 0) { perror("recvfrom error"); return false; } if(peer_ip!=NULL) *peer_ip = inet_ntoa(peer.sin_addr); if(peer_port!=NULL) *peer_port = ntohs(peer.sin_port); body->assign(tmp, ret); // 从tmp中取出ret长度的数据,放到body return true; } bool Send(const std::string &body, const std::string &peer_ip, uint16_t peer_port) { struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port = htons(peer_port); addr.sin_addr.s_addr = inet_addr(peer_ip.c_str()); socklen_t len = sizeof(struct sockaddr_in); // ssize_t sendto(int __fd, const void *__buf, size_t __n, int __flags, const sockaddr *__addr, socklen_t __addr_len) ssize_t ret = sendto(_sockfd, body.c_str(), body.size(), 0, (struct sockaddr *)&addr, len); if (ret < 0) { perror("sendto error"); return false; } return true; } bool Close() { if (_sockfd != -1) { close(_sockfd); _sockfd = -1; } return true; }};
TCP通信:
客户端向服务器发送一个请求,服务段处于listen监听状态,则会对这个连接请求进行处理:
1.为这个新链接请求,创建一个套接字结构体socket
2.为这个新的socket,描述完整的五元组信息(sip,sport,dip,dport,protocol)
往后的数据通信都是由这个新的套接字进行通信
一个服务器上有多少客户端想要简历连接,服务端就要创建多少个套接字
最早服务端创建的监听套接字–只负责新连接请求处理,不负责数据通信
服务端会为每个客户端都创建一个新的套接字,负责与这个客户端进行数据通信,但是想要通过这个套接字与客户顿进行通信,就要拿到这个套接字的描述符sockfd
相较于UDP套接字通信,TCP通信多了listen,connect,accept,
用recv,send简化了数据收发,因为地址信息都在sockfd描述的socket结构体中五元组完全包含,并且TCP通信时有状态的status
recv
这些调用返回接收到的字节数,如果发生错误,则返回-1。在如果发生错误,则设置errno以指示错误。
返回值将为0,说明没有数据,实际是对方已经执行有序关闭时,连接断开了
在客户端要注意监听套接字listenfd,通信套接字connfd的区别,listenfd只负责连接的建立在listen和accept时使用,当要发送数据是用accept返回的套接字connfd进行recv数据发送
tcpsocket.hpp(封装系统调用接口)
#ifndef __M_TCP_H__#define __M_TCP_H__#include #include #include #include #include #include #include #include #define MAX_LISTEN 1024class TcpSocket{private: /* data */ int _sockfd; // 这是监听套接字public: TcpSocket() : _sockfd(-1) {} ~TcpSocket() { Close(); _sockfd = -1; } bool Socket() { // int socket(int domain, int type, int protocol) _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (_sockfd < 0) { perror("socket error"); return false; } return true; } bool Bind(const std::string &ip, uint16_t port) { // int bind(int sockfd, struct sockaddr* addr, socklen_t len) struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); // port是2字节,要注意字节序问题,使用htons addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 192.168.154.131 ===> 转换成网络字节序的整形ip // 客户端bind的sockaddr要进行内容填充,将协议族,ip,端口都要确定 socklen_t addrlen = sizeof(struct sockaddr_in); // struct sockaddr: Structure describing a generic socket address. int ret = bind(_sockfd, (struct sockaddr *)&addr, addrlen); if (ret < 0) { perror("bind error"); return false; } return true; } bool Listen(int backlog = MAX_LISTEN) { // int listen(int backlog) int ret = listen(_sockfd, backlog); if (ret < 0) { perror("listen error"); return false; } return true; } bool Accept(TcpSocket &newsock) // 这里传入的newsock引用的是外部定义的新socket对象,用来保存通信套接字connfd,accept系统函数传入的是listenfd { // int accpet(int sockfd, struct sockaddr* peer,sock_len* len) int newfd = accept(_sockfd, (struct sockaddr *)NULL, NULL); // addr设置为NULL时,表示不关心客户端的地址,addrlen也应该设置为NULL if (newfd < 0) { perror("accept error"); return false; } newsock._sockfd = newfd; // 将获取的新建连接描述符,赋值给外部传入的TcpSocket对象 return true; } bool Recv(TcpSocket &sock, std::string &body) { // ssize_t recv(int sockfd, void* buf, int len, int flag);//收发数据的sockfd是accept获取的新建连接的描述符,不是监听Socket套接字 char tmp[1024] = {0}; // recv返回值Returns the number read or -1 for errors.为0时说明连接断开,所以也就没有数据 ssize_t ret = recv(sock._sockfd, tmp, 1023, 0); // flag=0,表示默认阻塞接收(接受缓冲区没有数据就阻塞) if (ret < 0) { perror("recv error"); return false; } else if (ret == 0) { std::cout << "connect broken"; return false; } body.assign(tmp, ret); // 从temp中截取ret大小数据放到body这个string对象中 return true; } bool Send(TcpSocket &sock, const std::string &body) { // ssize_t send(int sockfd, void* data, int len, int flag) ssize_t ret = send(sock._sockfd, body.c_str(), body.size(), 0); if (ret < 0) { perror("send error"); return false; } return true; } bool Connect(TcpSocket &sock, const std::string &ip, uint16_t port) // 客户端使用connect { // int connect(int sockfd, struct sockaddr* srvaddr, socklen_t len); struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(10001); addr.sin_addr.s_addr = inet_addr(ip.c_str()); socklen_t len = sizeof(struct sockaddr_in); int ret = connect(sock._sockfd, (struct sockaddr *)&addr, len); if (ret < 0) { perror("connect error"); return false; } return true; } bool Close() { if (_sockfd != 1) { close(_sockfd); _sockfd = -1; } return true; }};#endif
服务端s1.cpp
#include "tcpsocket.hpp"using namespace std;int main(int argc, char **argv){ TcpSocket tcpsocket; tcpsocket.Socket(); // uint16_t port = htons(stoi(argv[1])); string ip = string("192.168.154.131"); tcpsocket.Bind(ip, 10001); tcpsocket.Listen(); TcpSocket newsock; // 新建对象来保存connfd通信套接字(accept函数返回的) newsock.Socket(); tcpsocket.Accept(newsock); string buf; while (1) { tcpsocket.Recv(newsock, buf); cout << "client say:" << buf << endl; buf.clear(); cout << "server say:"; cin >> buf; tcpsocket.Send(newsock, buf); } tcpsocket.Close(); return 0;}
g++ -std=c++11 -o c1 c1.cpp
客户端c1.cpp
#include "tcpsocket.hpp"using namespace std;int main(int argc, char **argv){ TcpSocket tcp_cli; tcp_cli.Socket(); tcp_cli.Connect(tcp_cli, "192.168.154.131", (uint16_t)10001); string buf; while (1) { cout << "client say:"; cin >> buf; tcp_cli.Send(tcp_cli, buf); buf.clear(); tcp_cli.Recv(tcp_cli, buf); cout << "server say:" << buf << endl; } tcp_cli.Close();}
多线程实现
多线程实现tcp通信:
在单进程实现的问题,服务器只能与一个客户端进行通信
原因:因为不知道什么时候有数据到来和新连接到来,因此流程只能固定为获取新连接,然后进行通信,但是这两部都有可能会阻塞,一个执行流中要完成的事情太多了
解决:多执行流并发处理–获取新建连接后创建一个执行流专门负责与客户端的通信
多进程注意事项:1.僵尸子进程处理(SIGCHLD)2.描述符资源的释放(父子进程都有sockfd,父进程需要创建子进程后释放)
多线程注意事项:1.线程之间共享大部分进程数据资源(文件描述符表),因此通信套接字需要让负责通信的线程进行释放
如何在代码中知道连接断开了?连接断开后,在代码中如何体现的?
当recv函数接受数据时,返回值为0,代表的不仅仅是没有接收到数据,更多是为了表示连接断开了!!!
当send函数发送数据是,程序直接异常(SIGPIPE)退出,会显示坏管道
因此,如果网络通信中,不想让程序因为连接断开而导致发送数据的时候程序异常退出就对SIGPIPE信号进行signal处理
netstat -anptu