1、设计思路
(1) socket函数:
为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。
int socket(int family,int type,int protocol); //失败返回-1,并设置errno为相应的值,其它值为成功
第一个参数指明网络层协议簇,目前支持5种,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议);
第二个参数指明套接口类型,有四种类型可选:SOCK_STREAM(字节流套接口)、SOCK_DGRAM(数据报套接口)、SOCK_SEQPACKET(有序分组)和SOCK_RAW(原始套接口)。SOCK_STREAM基于TCP,是有保障的、面向连接的SOCKET,多用于资料(如文件)传送;SOCK_DGRAM基于UDP,是无保障的面向消息的socket , 主要用于在网络上发广播信息。
第三个参数指明相应的传输协议,常见的协议有TCP、UDP、SCTP,要指定它们分别使用宏IPPROTO_TCP、IPPROTO_UPD、IPPROTO_SCTP来指定,如果该值为0则使用默认值。
(2) 套接口地址结构:
这些地址结构的名字均已“sockaddr_”开头,并以对应每个协议族的唯一后缀结束。以IPv4套接口地址结构为例,它以“sockaddr_in”命名,以下是结构体的内容:
struct in_addr {
in_addr_t s_addr; /* IPv4地址 */
};
struct sockaddr_in {
uint8_t sin_len; /* 无符号的8位整数 */
sa_family_t sin_family; /* 套接口地址结构的地址簇,这里为AF_INET */
in_port_t sin_port; /* TCP或UDP端口 */
struct in_addr sin_addr;
char sin_zero[8];
};
(3) setsockopt和getsockopt函数:
(1) setsockopt()函数,用于任意类型、任意状态套接口的设置选项值。
int setsockopt(int scokfd,int level,int name,char *value,int *optlen);
(2) getsockopt()用于获取任意类型、任意状态套接口的选项当前值,并把结果存入optval。
int getsockopt(int sockfd,int level,int name,char *value,int optlen);
这两个函数参数相同如下:
第一个参数sockfd是套接字描述符,对于服务器是accept()函数返回的已连接套接字描述符,对于客户端是调用socket()函数返回的套接字描述符
第二个参数level是选项定义的层次,支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6
第三个参数optname是需设置的选项
第四个参数optval是指针,指向存放选项待设置的新值的缓冲区
第五个参数optlen是optval缓冲区长度
( 4 ) bind函数:
为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。
int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen); //返回0表示成功,-1表示失败
第一个参数是socket函数返回的套接口描述字;
第二个和第三个参数分别是一个指向特定于协议的地址结构的指针和该地址结构的长度。
(5) listen函数:
listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。
int listen(int sockfd,int backlog); //返回0表示成功,返回-1表示失败
第一个参数是socket函数返回的套接口描述字;
第二个参数规定了内核为此套接口排队的最大连接个数。由于listen函数第二个参数的原因,内核要维护两个队列:已完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手未完成的连接,accept函数是从已完成队列中取连接返回给进程;当已完成队列为空时,进程将进入睡眠状态。
( 6 ) accept函数:
accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。
int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen); //返回值非负表示成功,-1表示失败
第一个参数是socket函数返回的套接口描述字;第二个和第三个参数分别是一个指向连接方的套接口地址结构和该地址结构的长度;该函数返回的是一个全新的套接口描述字;如果对客户段的信息不感兴趣,可以将第二和第三个参数置为空。
( 7 ) write和read函数:
当服务器和客户端的连接建立起来后,就可以进行数据传输了,服务器和客户端用各自的套接字描述符进行读/写操作。因为套接字描述符也是一种文件描述符,所以可以用文件读/写函数write()和read()进行接收和发送操作。
(1) write()函数用于数据的发送。
int write(int sockfd, char *buf, int len); //返回值非负表示成功,-1表示失败
(2) read()函数用于数据的接收。
int read(int sockfd, char *buf, int len); //返回值非负表示成功,-1表示失败
两个函数参数相同如下:
第一个参数sockfd是套接字描述符,对于服务器是accept()函数返回的已连接套接字描述符,对于客户端是调用socket()函数返回的套接字描述符;
第二个参数buf是指向一个用于发送 / 接收信息的数据缓冲区;
第三个参数len指明发送 / 接收数据缓冲区的大小。
( 8 ) send和recv函数:
TCP套接字提供了send()和recv()函数,用来发送和接收操作。这两个函数与write()和read()函数很相似,只是多了一个附加的参数。
(1) send()函数用于数据的发送。
ssize_t send(int sockfd, const void *buf, size_t len, int flags); //成功返回写出的字节数,失败返回-1
前3个参数与write()相同,参数flags是传输控制标志。
(2) recv()函数用于数据的发送。
ssize_t recv(int sockfd, void *buf, size_t len, int flags); //成功返回读入的字节数,失败返回-1
前3个参数与read()相同,参数flags是传输控制标志。
( 9 ) close函数:用于关闭socket
2、实现代码
TCP服务器端:Windows和Linux皆可
// ServerMain.cpp : Linux/windows Server
// socket()->bind()->listen()->accept()->read()->write()->close()
#include <stdint.h>
#include <stdio.h>
#ifdef _MSC_VER
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#endif
bool CloseSerSocket(uint32_t dwSockId,const char* strError)
{
#ifdef _MSC_VER
closesocket(dwSockId);
#else
close(dwSockId);
#endif
if (NULL != strError)
{
perror(strError);
return false;
}
else
{
return true;
}
}
int main()
{
#ifdef _MSC_VER
WORD wVersion;
WSADATA wsData;
wVersion = MAKEWORD(1, 1);
if (WSAStartup(wVersion, &wsData) != 0)
{
perror("windows network init error");
return false;
}
int nOn = 1000;
#else
struct timeval nOn = { 1,0 };
#endif
// 初始化Socket
uint32_t dwSocketId = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (dwSocketId == -1)
{
return CloseSerSocket(dwSocketId, "init socket error, can not get file description");
}
// 设置地址重用
if (setsockopt(dwSocketId, SOL_SOCKET, SO_REUSEADDR, (const char*)&nOn, sizeof(nOn)) == -1)
{
return CloseSerSocket(dwSocketId, "set socket error");
}
// 设置服务器信息
sockaddr_in stServerAddr;
sockaddr_in stClientAddr;
stServerAddr.sin_family = AF_INET;
stServerAddr.sin_port = htons(5050);
stServerAddr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY就是inet_addr("0.0.0.0") 当服务器的监听地址是INADDR_ANY时,就是所有的都监听
// 绑定socket
if (bind(dwSocketId,(const sockaddr*)&stServerAddr,sizeof(stServerAddr)) == -1)
{
return CloseSerSocket(dwSocketId, "bind socket error");
}
// 监听客户端请求
if (listen(dwSocketId, 100) == -1)
{
return CloseSerSocket(dwSocketId, "listen socket error");
}
socklen_t nCliSize = 0;
uint32_t dwSocketFd = 0;
char reveBuff[2048];
char sendBuff[2048];
while (true)
{
memset(reveBuff, '\0', sizeof(reveBuff));
memset(sendBuff, '\0', sizeof(sendBuff));
nCliSize = sizeof(stClientAddr);
// 从已完成连接队列头返回一个已完成连接
if ((dwSocketFd = accept(dwSocketId, (sockaddr*)&stClientAddr, &nCliSize)) == -1)
{
return CloseSerSocket(dwSocketId, "accept socket error");
}
// 接收客户端包
if (recv(dwSocketFd, reveBuff, 2048, 0) == -1)
{
return CloseSerSocket(dwSocketId, "recv package error");
}
sprintf(sendBuff, "i have receive the message:%s from you(%s)", reveBuff, inet_ntoa(stClientAddr.sin_addr));
// 返回客户包
if (send(dwSocketFd, sendBuff, strlen(sendBuff), 0) == -1)
{
return CloseSerSocket(dwSocketId, "send package error");
}
}
CloseSerSocket(dwSocketId, NULL);
return 0;
}
TCP客户端:仅限Windows端
// ClientMain.cpp : Windows Only
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
bool CloseCliSocket(uint32_t dwSockId, const char* strError)
{
closesocket(dwSockId);
if (NULL != strError)
{
perror(strError);
system("pause");
return false;
}
else
{
return true;
}
}
int main()
{
WORD wVersion;
WSADATA wsData;
wVersion = MAKEWORD(1, 1);
if (WSAStartup(wVersion,&wsData) != 0)
{
perror("windows network init error");
return false;
}
// 初始化Socket
uint32_t dwSocketId = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (dwSocketId == -1)
{
return CloseCliSocket(dwSocketId, "init socket error, can not get file description");
}
// 设置服务器信息
sockaddr_in stServerAddr;
stServerAddr.sin_family = AF_INET;
stServerAddr.sin_port = htons(5050);
stServerAddr.sin_addr.s_addr = inet_addr("192.168.1.5");
if (connect(dwSocketId, (const sockaddr*)&stServerAddr, sizeof(stServerAddr)) == -1)
{
return CloseCliSocket(dwSocketId, "connect server error");
}
char delBuff[2048];
sprintf(delBuff, "%s", "hello");
// 发送包
if (send(dwSocketId, delBuff, strlen(delBuff), 0) == -1)
{
return CloseCliSocket(dwSocketId, "send package error");
}
memset(delBuff, '\0', sizeof(delBuff));
// 接收包
if (recv(dwSocketId, delBuff, 2048, 0) == -1)
{
return CloseCliSocket(dwSocketId, "recv package error");
}
printf("%s\n", delBuff);
CloseCliSocket(dwSocketId, NULL);
system("pause");
return 0;
}