套接字編程:如何編寫一個網(wǎng)絡(luò)通信程序
1.網(wǎng)絡(luò)通信的數(shù)據(jù)中都會包含一個完整的五元組:
sip,sport,dip,dport,protocol(源IP,源端口,對端IP,對端端口,協(xié)議)
五元組完整的描述了數(shù)據(jù)從哪來,到哪去,用什么數(shù)據(jù)格式
2.網(wǎng)絡(luò)通信–兩個主機進程之間的通信:客戶端&服務端
客戶端:用戶使用,發(fā)起請求
服務端:網(wǎng)絡(luò)應用提供商提供服務的程序(后臺開發(fā))
UDP協(xié)議通信:
服務端一方要提前啟動,保證有數(shù)據(jù)到來時,一定能夠接收;并且服務端永遠都是先接收數(shù)據(jù),因為服務端這一方并沒有保存客戶端的地址,沒有地址綁定,也就沒有數(shù)據(jù)來源,也不知道數(shù)據(jù)要發(fā)送什么,要發(fā)給誰
客戶端:創(chuàng)建套接字,端口綁定(不推薦),發(fā)送數(shù)據(jù),接收數(shù)據(jù),關(guān)閉套接字
操作接口:
socket–創(chuàng)建套接字
domain—地址域類型(域間通信、IPv4通信、IPv6通信)AF_INET—IPv4網(wǎng)絡(luò)協(xié)議
type–套接字類型
SOCK_STREAM ; SOCK_DGRAM
bind–為套接字綁定地址信息
int bind(int sockfd, struct sockaddr *addr, socklen_t address_len)
三個參數(shù):套接字描述符,要綁定的地址(不同地址域,有不同的地址結(jié)構(gòu)),sockaddr結(jié)構(gòu)體的長度
sendto:發(fā)送數(shù)據(jù)
參數(shù):sockfd—返回的套接字描述符
buf–要發(fā)送的數(shù)據(jù)空間起始地址
len–要發(fā)送的數(shù)據(jù)長度(從buf地址開始,發(fā)送len長度的數(shù)據(jù))
flags–默認0-阻塞發(fā)送(發(fā)送緩沖區(qū)滿了就等著)
dest_addr–對端地址信息,數(shù)據(jù)要發(fā)送給誰,目的地
addrlen–對端地址信息長度
recvfrom:接收數(shù)據(jù)
對于recvfrom的src_addr參數(shù),是為了獲取數(shù)據(jù)是誰發(fā)送的,相當于用指針接收返回值,addrlen也一樣(可以看到這里的addrlen是指針)
int close(int fd)
關(guān)閉套接字,釋放資源
字節(jié)序相關(guān)接口:
htonl(32位),htons(16位),ntohl,ntohs ; l–32位 ; s–16位
這幾個接口已經(jīng)進行了主機字節(jié)序的判斷,因此無需擔心自己的主機字節(jié)序
32位數(shù)據(jù)轉(zhuǎn)換接口與16位不能混用,會出現(xiàn)數(shù)據(jù)截斷,就像漢字用兩個字節(jié)表示,而如果按字節(jié)輸出就會亂碼,并且再大小端轉(zhuǎn)換時,數(shù)據(jù)截斷會造成數(shù)據(jù)的錯誤
sockaddr結(jié)構(gòu)體
udp_srv.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>//字節(jié)序轉(zhuǎn)換接口頭文件
#include <netinet/in.h>//地址結(jié)構(gòu)類型以及協(xié)議類型宏頭文件
#include <sys/socket.h>//套接字接口頭文件
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]); // 輸入的參數(shù)都是按字符串存的,這里將端口port轉(zhuǎn)int
char *ip = argv[1];
// 創(chuàng)建套接字 int socket(int domain, int type ,int protocol)
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // ipv4域,數(shù)據(jù)報,udp協(xié)議
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;
}
// 循環(huán)接收發(fā)送數(shù)據(jù)
while (1)
{
char buf[1024] = {0};
struct sockaddr_in peer; // 地址由系統(tǒng)設(shè)置,數(shù)據(jù)誰發(fā)的,設(shè)置的就是誰
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); // 轉(zhuǎn)成字符串
uint16_t peerport = ntohs(peer.sin_port);
printf("client[%s:%d] say: %s\n", peerip, peerport, buf);
// 發(fā)送數(shù)據(jù)
// 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;
}
}
// 關(guān)閉套接字
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 <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <cstdio>
#include <cassert>
#include <string>
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長度的數(shù)據(jù),放到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通信:
客戶端向服務器發(fā)送一個請求,服務段處于listen監(jiān)聽狀態(tài),則會對這個連接請求進行處理:
1.為這個新鏈接請求,創(chuàng)建一個套接字結(jié)構(gòu)體socket
2.為這個新的socket,描述完整的五元組信息(sip,sport,dip,dport,protocol)
往后的數(shù)據(jù)通信都是由這個新的套接字進行通信
一個服務器上有多少客戶端想要簡歷連接,服務端就要創(chuàng)建多少個套接字
最早服務端創(chuàng)建的監(jiān)聽套接字–只負責新連接請求處理,不負責數(shù)據(jù)通信
服務端會為每個客戶端都創(chuàng)建一個新的套接字,負責與這個客戶端進行數(shù)據(jù)通信,但是想要通過這個套接字與客戶頓進行通信,就要拿到這個套接字的描述符sockfd
相較于UDP套接字通信,TCP通信多了listen,connect,accept,
用recv,send簡化了數(shù)據(jù)收發(fā),因為地址信息都在sockfd描述的socket結(jié)構(gòu)體中五元組完全包含,并且TCP通信時有狀態(tài)的status
recv
這些調(diào)用返回接收到的字節(jié)數(shù),如果發(fā)生錯誤,則返回-1。在如果發(fā)生錯誤,則設(shè)置errno以指示錯誤。
返回值將為0,說明沒有數(shù)據(jù),實際是對方已經(jīng)執(zhí)行有序關(guān)閉時,連接斷開了
在客戶端要注意監(jiān)聽套接字listenfd,通信套接字connfd的區(qū)別,listenfd只負責連接的建立在listen和accept時使用,當要發(fā)送數(shù)據(jù)是用accept返回的套接字connfd進行recv數(shù)據(jù)發(fā)送
tcpsocket.hpp(封裝系統(tǒng)調(diào)用接口)
#ifndef __M_TCP_H__
#define __M_TCP_H__
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <cstdio>
#include <cassert>
#include <string>
#define MAX_LISTEN 1024
class TcpSocket
{
private:
/* data */
int _sockfd; // 這是監(jiān)聽套接字
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字節(jié),要注意字節(jié)序問題,使用htons
addr.sin_addr.s_addr = inet_addr(ip.c_str()); // 192.168.154.131 ===> 轉(zhuǎn)換成網(wǎng)絡(luò)字節(jié)序的整形ip
// 客戶端bind的sockaddr要進行內(nèi)容填充,將協(xié)議族,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系統(tǒng)函數(shù)傳入的是listenfd
{
// int accpet(int sockfd, struct sockaddr* peer,sock_len* len)
int newfd = accept(_sockfd, (struct sockaddr *)NULL, NULL); // addr設(shè)置為NULL時,表示不關(guān)心客戶端的地址,addrlen也應該設(shè)置為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);//收發(fā)數(shù)據(jù)的sockfd是accept獲取的新建連接的描述符,不是監(jiān)聽Socket套接字
char tmp[1024] = {0};
// recv返回值Returns the number read or -1 for errors.為0時說明連接斷開,所以也就沒有數(shù)據(jù)
ssize_t ret = recv(sock._sockfd, tmp, 1023, 0); // flag=0,表示默認阻塞接收(接受緩沖區(qū)沒有數(shù)據(jù)就阻塞)
if (ret < 0)
{
perror("recv error");
return false;
}
else if (ret == 0)
{
std::cout << "connect broken";
return false;
}
body.assign(tmp, ret); // 從temp中截取ret大小數(shù)據(jù)放到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函數(shù)返回的)
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();
}
多線程實現(xiàn)
多線程實現(xiàn)tcp通信:
在單進程實現(xiàn)的問題,服務器只能與一個客戶端進行通信
原因:因為不知道什么時候有數(shù)據(jù)到來和新連接到來,因此流程只能固定為獲取新連接,然后進行通信,但是這兩部都有可能會阻塞,一個執(zhí)行流中要完成的事情太多了
解決:多執(zhí)行流并發(fā)處理–獲取新建連接后創(chuàng)建一個執(zhí)行流專門負責與客戶端的通信
多進程注意事項:1.僵尸子進程處理(SIGCHLD)2.描述符資源的釋放(父子進程都有sockfd,父進程需要創(chuàng)建子進程后釋放)
多線程注意事項:1.線程之間共享大部分進程數(shù)據(jù)資源(文件描述符表),因此通信套接字需要讓負責通信的線程進行釋放
如何在代碼中知道連接斷開了?連接斷開后,在代碼中如何體現(xiàn)的?
當recv函數(shù)接受數(shù)據(jù)時,返回值為0,代表的不僅僅是沒有接收到數(shù)據(jù),更多是為了表示連接斷開了?。?!
當send函數(shù)發(fā)送數(shù)據(jù)是,程序直接異常(SIGPIPE)退出,會顯示壞管道
因此,如果網(wǎng)絡(luò)通信中,不想讓程序因為連接斷開而導致發(fā)送數(shù)據(jù)的時候程序異常退出就對SIGPIPE信號進行signal處理文章來源:http://www.zghlxwxcb.cn/news/detail-406876.html
netstat -anptu文章來源地址http://www.zghlxwxcb.cn/news/detail-406876.html
到了這里,關(guān)于網(wǎng)絡(luò)編程2(套接字編程)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!