目录
一、网络通信socket
计算机是通过TCP/IP协议进行互联从而进行通信的,为了把复杂的TCP/IP协议隐藏起来,更方便的实现计算机中两个程序进行通信,引出了socket这个概念。
socket翻译为套接字,可以理解为IP地址与端口号的组合。
socket提供了流(stream)和数据报(datagram)两种通信机制,即流socket(SOCK_STREAM)和数据报socket(SOCK_DGRAM)。
流socket基于TCP协议,是一个有序、可靠、双向字节流的通道,传输数据不会丢失、不会重复、顺序也不会错乱。就像两个人在打电话,接通后就在线了,您一句我一句的聊天。
数据报socket基于UDP协议,不需要建立和维持连接,可能会丢失或错乱。UDP不是一个可靠的协议,对数据的长度有限制,但是它的速度比较高。就像短信功能,一个人向另一个人发短信,对方不一定能收到。
在实际开发中,数据报socket的应用场景极少,绝大部分情况下使用流套接字(SOCK_STREAM)。
二、网络通信的过程
在TCP/IP网络应用中,两个程序之间通信模式是客户/服务端模式(client/server),客户/服务端也叫作客户/服务器。服务端与客户端网络通信的交互过程如下图所示。
图中使用了socket(),connect等函数是网络通信常用的API。对于程序员来说,只要用好socket相关的函数,就可以完成网络通信。
三、网络编程常用API
A、客户端网络通信过程与常用API
根据客户端的工作流程一次介绍如下套接字基本函数:
- socket()
- connect()
- send()/recv()
- close()
1, 创建套接字:int socket(int family,int type,int protocol);
功能介绍:在Linux系统中,一切皆文件。为了表示和区分已经打开的文件,UNIX/Linux会给文件分配一个ID,这个ID就是一个整数,被称为文件描述符。因此,网络连接也是一个文件,它也有文件描述符。通过socket()函数来创建一个网络连接或者说打开一个网络文件,socket()函数的返回值就是文件描述符,通过这个文件描述符我们就可以使用普通的文件操作来传输数据了。
参数介绍:
参数一:family 代表协议族,在socket中只能是AF_INET。
参数二:type 代表协议类型常见类型是SOCK_STREAM(TCP),SOCK_DGRAM(UDP)
参数三:protocol 代表具体的协议,对于标准套接字来说,其值是0。(原始套接字基本不会使用)
2,客户端请求连接函数: int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);
功能介绍:客户端向服务端发起连接请求,当返回值是0时代表连接成功,返回值为-1时代表连接失败。
参数介绍:
参数一:sock_fd代表通过socket()函数返回的文件描述符
参数二:serv_addr 代表目标服务器的协议族,网络地址以及端口号。是一个sockaddr 类型的指针。
参数三:addrlen 代表第二个参数内容的大小。
3,客户端发送与接收数据函数:
3.1,int send(int sockfd,const void *buf,int len,int flags);
功能介绍:send函数用于把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。客户端通过该函数向服务器应用程序发送数据。函数返回已发送的字节数。出错时返回-1
参数介绍:
参数一:sockfd代表 发送端的套接字描述符,即通过socket()函数返回的文件描述符。
参数二:buf 指明需要发送数据的内存地址,可以是C语言基本数据类型变量的地址,也可以是数组、结构体、字符串。
参数三: len 指明实际发送数据的字节数。
参数四:flags 一般设置为0,其他数值意义不大。
3.2,int recv(int sockfd,void *buf,int len,int flags);
功能介绍:recv函数用于接收对端socket发送过来的数据。不论是客户端还是服务端,应用程序都用recv函数接受来自TCP连接的另一端发送过来的数据。
如果socket对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数。出错时返回-1。如果socket被对端关闭,返回值为0。
参数介绍:
参数一:sockfd代表接收端的套接字描述符,即通过socket()函数返回的文件描述符。
参数二:buf 为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以是数组、结构体、字符串。只要是一块内存就行了。
参数三:len 指明需要接收数据的字节数。不能超过buf的大小,否则内存溢出。
参数四:flags填0,其他数值意义不大。
4,int close(int sockfd);
函数功能:关闭套接字,并终止TCP连接。若成功则返回0.失败则返回-1;
函数参数:
参数一:sockfd代表接收端的套接字描述符,即通过socket()函数返回的文件描述符。
B、服务端网络通信过程与常用API
根据客户端的工作流程一次介绍如下套接字基本函数:
- socket()
- bind()
- listen()
- accept()
- recv()/send()
- close()
一部分函数已经介绍过,接下来介绍另一部分函数。
C、服务端网络通信过程与常用API
1,套接字绑定函数:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数功能:服务端把用于通信的地址和端口绑定到socket上,当bind函数返回0时,为正确绑定,返回-1,则为绑定失败。
参数介绍:
参数一:sockfd 代表需要绑定的socket。是在创建socket套接字时返回的文件描述符。
参数二:addr 存放了服务端用于通信的地址和端口。
参数三:addrlen 代表addr结构体的大小。
2,监听函数:int listen(int sockfd, int backlog);
函数功能:listen函数的功能并不是等待一个新的connect的到来,真正等待connect的是accept函数。isten的操作就是当有较多的client发起connect时,server端不能及时的处理已经建立的连接,这时就会将connect连接放在等待队列中缓存起来。这个等待队列的长度有listen中的backlog参数来设定。当listen运行成功时,返回0;运行失败时,返回值为-1.
参数说明:
参数一:sockfd是前面socket创建的文件描述符;
参数二:backlog是指server端可以缓存连接的最大个数,也就是等待队列的长度。
3,接收请求函数:int accept(int sockfd,struct sockaddr *client_addr,socklen_t *addrlen);
函数功能:accept函数等待客户端的连接,如果没有客户端连上来,它就一直等待,这种方式称为阻塞。accept等待到客户端的连接后,创建一个新的socket,函数返回值就是这个新的socket,服务端用于这个新的socket和客户端进行报文的收发。
参数介绍:
参数一:sockfd 是已经被listen过的socket。
参数二:client_addr 用于存放客户端的地址信息,其中包含客户端的协议族,网络地址以及端口号。如果不需要客户端的地址,可以填0。
参数三:addrlen 用于存放参数二(client_addr)的长度
四、C++网络编程小案例
以下是写的Linux系统的一个服务器与多个客户端通信的代码,其中有使用到多线程。没学过多线程的还需要先去学习一下C语言多线程技术。
其中服务器代码为server.c。
下面展示服务器的全部代码 server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#define nums 128
typedef struct
{
int fd; // 通信
pthread_t tid; // 线程ID
struct sockaddr_in addr; // 地址信息
}SockInfo;
SockInfo infos[nums];
void* working(void* arg) {
SockInfo* info = (SockInfo*)arg;
//打印客户端的地址信息
char ip[32] = { 0 }; //网络ip转本地小端的函数inet_ntop,第三、四个参数需要
printf("client ip is: %s, nport is: %d\n", inet_ntop(AF_INET, &info->addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(info->addr.sin_port));
//5.和客户端通信
while (1)
{
//接收数据
char buffer[1024];
//接收函数 recv 或 read都可以
int len = recv(info->fd, buffer, sizeof(buffer), 0);
if (len > 0) {
printf("client say: %s\n", buffer);
//回复客户端
send(info->fd, buffer, len, 0);
}
else if (len == 0) {
printf("client was close...\n");
break;
}
else {
perror("recv");
break;
}
}
close(info->fd);
info->tid = -1;
info->fd = -1;
return NULL;
}
int main()
{
// 1.创建监听的套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
if (lfd==-1) {
perror("socket");
exit(0);
}
//2.将监听套接字与本地ip、端口绑定在一起
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(30001); //转成大端端口
/*
INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
这个宏可以代表任意一个IP地址
这个宏一般用于本地的绑定操作
*/
addr.sin_addr.s_addr = INADDR_ANY; // 这个宏的值为0 == 0.0.0.0
//绑定
int ret = bind(lfd,(struct sockaddr*) &addr, sizeof(addr));
if (ret==-1) {
perror("bind");
exit(0);
}
//3.设置监听
int lit = listen(lfd,128); //最多128个
if (lit == -1) {
perror("listen");
exit(0);
}
//初始化infos
for (int i = 0; i < nums; i++)
{
bzero(&infos[i], sizeof(infos[i])); //结构体初始化为0
infos[i].fd = -1;
infos[i].tid = -1;
}
//主线程监听并创建子线程通信
while (1)
{
SockInfo* ptr;
//从infos结构体找一个未使用的
for (int i = 0; i < nums; i++)
{
if (infos[i].fd == -1) {
ptr = &infos[i];
break;
}
//如果客户端占满了,睡眠1秒等待新的客户端空闲位置
if (i == nums - 1)
{
sleep(1);
i--;
}
}
//4.阻塞等待并接收客户端连接
int cliaddr_len = sizeof(struct sockaddr_in);
int clienfd = accept(lfd, (struct sockaddr*)&ptr->addr, &cliaddr_len); //accept函数返回通信套接字clienfd
printf("parent thread, connfd: %d\n", clienfd);
if (clienfd == -1) {
perror("accept");
exit(0);
}
//创建子线程
ptr->fd = clienfd;
pthread_create(&ptr->tid,NULL,working,ptr);
//让子线程和主线程分离
pthread_detach(ptr->tid);
}
//5.断开连接, 关闭套接字
close(lfd); // 监听套接字
return 0;
}
客户端Linux系统的代码。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#define IP "服务器的ip地址"
int main()
{
// 1.创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket");
exit(0);
}
//2.连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(30001); //转成大端端口
inet_pton(AF_INET,IP,&addr.sin_addr.s_addr);
//连接
int ret = connect(fd,(struct sockaddr*) &addr,sizeof(addr));
if (ret == -1) {
perror("connect");
exit(0);
}
//3.和服务器通信
int number = 0; //通信次数
while (1)
{
char buffer[1024];
sprintf(buffer,"hello,server...%d\n",number++);
//发数据到服务器
send(fd,buffer,strlen(buffer)+1,0);
//接收服务器传来的数据
memset(buffer,0,sizeof(buffer));
int len = recv(fd,buffer,sizeof(buffer),0);
if (len > 0 ) {
printf("server say: %s\n",buffer);
}
else if (len==0) {
printf("server was close...\n");
break;
}
else {
perror("recv");
break;
}
sleep(1); //每隔1秒发送一条数据
}
//4.断开连接, 关闭套接字
close(fd);
return 0;
}
客户端Windows系统的代码。
【注意】:在windows系统上,#include <arpa/inet.h>没用,需要更改为#include <winsock2.h> ,并且需要连接动态库 -lws2_32。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <winsock2.h>
#define IP "服务器的ip地址"
int main()
{
WSADATA wsa; // 正确的类型名
int result;
// 初始化 Winsock
result = WSAStartup(MAKEWORD(2,2), &wsa);
// 1.创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket");
exit(0);
}
//2.连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(30001); //转成大端端口
inet_pton(AF_INET,IP,&addr.sin_addr.s_addr);
//连接
int ret = connect(fd,(struct sockaddr*) &addr,sizeof(addr));
if (ret == -1) {
perror("connect");
exit(0);
}
//3.和服务器通信
int number = 0; //通信次数
while (1)
{
char buffer[1024];
sprintf(buffer,"hello,server...%d\n",number++);
//发数据到服务器
send(fd,buffer,strlen(buffer)+1,0);
//接收服务器传来的数据
memset(buffer,0,sizeof(buffer));
int len = recv(fd,buffer,sizeof(buffer),0);
if (len > 0 ) {
printf("server say: %s\n",buffer);
}
else if (len==0) {
printf("server was close...\n");
break;
}
else {
perror("recv");
break;
}
sleep(1); //每隔1秒发送一条数据
}
//4.断开连接, 关闭套接字
close(fd);
// 注销Winsock相关库
WSACleanup();
return 0;
}
以上就是一个简易的利用Socket+多线程在Linunx及Windows上实现的网络编程小案例,记录生活、记录美好,希望可以帮助到正在浏览的你。
一部分内容来源于CSDN博主「镜花寒」
原文链接:https://blog.csdn.net/ZH0314/article/details/77387162