参考文章
https://blog.csdn.net/m0_37947204/article/details/80489431
https://www.cnblogs.com/fengff/p/10984251.html
代码来源:B站码农论坛
//server.cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
if (argc!=2)
{
printf("Using:./server port\nExample:./server 5005\n\n"); return -1;
}
// 第1步:创建服务端的socket。
int listenfd; // fd 文件描述符 0 自己创建的从3开始
if ( (listenfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }
// 第2步:把服务端用于通信的地址和端口绑定到socket上。
struct sockaddr_in servaddr; // 服务端地址信息的数据结构。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。
servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口,使用网络字节顺序
if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{ perror("bind"); close(listenfd); return -1; }
// 第3步:把socket设置为监听模式。
if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }
// 第4步:接受客户端的连接。
int clientfd; // 客户端的socket。
int socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小
struct sockaddr_in clientaddr; // 客户端的地址信息。
//从已准备好的连接队列中获取一个请求,如果对列为空,accept函数将阻塞等待
clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
printf("客户端(%s)已连接。\n",inet_ntoa(clientaddr.sin_addr));
// 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
char buffer[1024];
while (1)
{
int iret;
memset(buffer,0,sizeof(buffer));
if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) // 接收客户端的请求报文。
{
printf("iret=%d\n",iret); break;
}
printf("接收:%s\n",buffer);
strcpy(buffer,"ok");
if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) // 向客户端发送响应结果。
{ perror("send"); break; }
printf("发送:%s\n",buffer);
}
// 第6步:关闭socket,释放资源。
close(listenfd); close(clientfd);
}
//client.cpp
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
if (argc != 3)
{
printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n");
return -1;
}
// 第1步:创建客户端的socket。
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){ perror("socket"); return -1;}
// 第2步:向服务器发起连接请求。
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
struct hostent *h;
if ((h = gethostbyname(argv[1])) == 0) // 指定服务端的ip地址。
{
printf("gethostbyname failed.\n");
close(sockfd);
return -1;
}
memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) // 向服务端发起连接清求。
{
perror("connect");
close(sockfd);
return -1;
}
char buffer[1024];
// 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
for (int ii = 0; ii < 3; ii++)
{
int iret;
memset(buffer, 0, sizeof(buffer));
sprintf(buffer, "这是第%d个,编号%03d。", ii + 1, ii + 1);
if ((iret = send(sockfd, buffer, strlen(buffer), 0)) <= 0) // 向服务端发送请求报文。
{
perror("send");
break;
}
printf("发送:%s\n", buffer);
memset(buffer, 0, sizeof(buffer));
if ((iret = recv(sockfd, buffer, sizeof(buffer), 0)) <= 0) // 接收服务端的回应报文。
{
printf("iret=%d\n", iret);
break;
}
printf("接收:%s\n", buffer);
sleep(1);
}
// 第4步:关闭socket,释放资源。
close(sockfd);
}
一、使用socket创建套接字
//函数原型
int socket(int domain,int type,int protpcol);
- 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 为数据传输方式,常用的有 SOCK_STREAM(面向连接) 和 SOCK_DGRAM(无连接)
- protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
一般第一个参数填AF_INET,第二个参数填SOCK_STREAM,第三个参数为0。
返回值:成功返回一个socket,失败返回-1,错误原因存于errno中
//fd 文件描述符(file descriptor)
//0为标准输入,1为标准输出,2为标准错误 自己创建的一般从3开始
int listenfd = socket(AF_INET,SOCK_STREAM,0)
if ( (listenfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }
二、绑定通信地址和端口
struct sockaddr_in servaddr; // 服务端地址信息的数据结构。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。
/*
//可以解析域名和主机名 兼容性更好
struct hostent *h;
if ((h = gethostbyname(argv[1])) == 0) // 指定服务端的ip地址。
{
printf("gethostbyname failed.\n");
close(sockfd);
return -1;
}
memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);
*/
servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口,使用网络字节顺序
if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{ perror("bind"); close(listenfd); return -1; }
结构体
struct sockaddr{
unsigned short sa_family; //地址类型 AF_xxx
char sa_data[14]; //14字节的端口和地址
};
struct sockaddr_in{
short int sin_family; //地址类型
struct in_addr sin_addr; //地址
unsigned short int sin_port; //端口号
unsigned char sin_zero[8]; //为了和struct sockaddr保持一样的长度,方便转化
};
struct in_addr{
unsigned long s_addr; //地址
};
——————————————————————————————————————————————————————
struct hostent{
char *h_name; //主机名
char **h_aliases; //主机所有别名构成的字符串数组,同一IP可绑定多个域名
int h_addrtype; //主机IP地址的类型
int h_length; //主机IP地址的长度,IPV4为4,IPV6为16
char **h_addr_list; //主机的IP地址,以网络字节序存储
};
#define h_addr h_addr_list[0]
//gethostbyname函数可以利用字符串格式的域名获得IP网络字节顺序地址
struct hostent *gethostbyname(const char *name);
网络字节序和主机字节序之间的转换函数
网络字节序为大端
htons()、ntohs()完成16位无符号数的相互转换
htonl()、 ntohl()完成32位无符号数的相互转换
host to network short/long
一些地址转换函数
1) int inet_aton(const char *cp,struct in_addr *inp);
//将一个字符串IP地址转换为一个32位的网络字节序IP地址,成功返回非零,失败返回0
2) char *inet_ntoa(struct in_addr in);
//将网络字节序IP地址转换成字符串的IP地址
3) in_addr_t inet_addr(const char *cp);
//功能和第一个一样
bind()函数
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
//sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出
三、listen()、connect()和accept()函数
int listen(int sock, int backlog);
listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求
sock 为需要进入监听状态的套接字,backlog表示已完成连接的最大数量,满了之后的请求会放在半连接队列里。
如果没有错误发生,listen()返回0,失败返回-1。
注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
sock是欲建立连接的本地套接字描述符。serv_addr指出说明对方套接字地址结构的指针,对方套接字地址长度由addrlen说明。
成功则返回0,失败返回-1。
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。
sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。
注意:accept()返回的是一个新的套结字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
四、数据传输—send()和recv()函数
连接建立后,接下来就要传输数据了。
send函数用于把数据通过socket发送给对端,无论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据
//函数声明
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd为已经建立好连接的socket
buf为需要发送的数据的内存地址,基本数据变量、数组、字符串都行,内存中有什么发什么
len为需要发送数据的长度,为buf中有效数据的长度
flags 指定传输控制方式,填0
函数返回已发送的字符数,出错时返回-1
注意:如果网络断开或socket已被对端关闭,send函数要过几秒才会报错
如果send函数返回<=0,表示通信链路已不可用
recv函数用于接收对端通过socket发送过来的数据,不论是客户端还是服务端,应用程序都用recv函数接收来自向TCP连接的另一端发送的数据
//函数声明
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd为已经建立好连接的socket
buf为用于接收数据的内存地址,只要是一块内存就行
len为需要接收数据的长度,不能超过buf的大小,否则内存溢出
flags填0
函数返回已接收的字符数,出错时返回-1
如果socket对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数,出错时返回-1,如果socket被对端关闭,返回0。
如果recv函数返回的错误<=0,表示通信通道已不可用