socket编程学习笔记

该文章详细介绍了如何使用C++实现基于TCP/IP的服务器和客户端程序。通过创建socket、绑定地址和端口、监听连接、接受客户端连接以及进行数据传输,展示了服务器端如何等待并处理客户端的请求,以及客户端如何向服务器发起连接并发送接收数据。涉及到的关键函数包括socket、bind、listen、accept、connect、send和recv。
摘要由CSDN通过智能技术生成

参考文章
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);
  1. 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。
  2. type 为数据传输方式,常用的有 SOCK_STREAM(面向连接) 和 SOCK_DGRAM(无连接)
  3. 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
2char *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,表示通信通道已不可用

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值