服务端:
1、通过socket()函数创建用于接收连接请求的socket
2、构造主机连接地址的sockaddr_in结构体,包括sin_family,sin_port,sin_addr三个成员
3、绑定sockaddr_in结构体和socket
4、通过listen()函数将stocket设置为监听模式
5、调用accept()函数等待接收客户端的连接请求,并返回用于传递信息的stocket
6、和客户端进行通信
7、调用close()关闭socket
// 简单的TCP回送服务 服务端示例
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
if (argc!=2)
{
printf("Using:./server port\nExample:./server 5005\n\n"); return -1;
}
// 第1步:通过socket()函数创建用于接收连接请求的socket
int listenfd;
if ( (listenfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }
// 第2步:构造主机连接地址的sockaddr_in结构体,包括sin_family,sin_port,sin_addr三个成员
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("111.111.111.111"); // 指定ip地址。
servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。
// 第3步: 绑定sockaddr_in结构体和socket
if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
{ perror("bind"); close(listenfd); return -1; }
// 第4步:通过listen()函数将stocket设置为监听模式
if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }
// 第5步:调用accept()函数等待接收客户端的连接请求,并返回用于传递信息的stocket
// 这里也可以调用高级IO函数 或 使用多进程/线程的模式 实现与多个客户端同时通讯
int clientfd; // 客户端的socket。
int socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小
struct sockaddr_in clientaddr; // 客户端的地址信息。
clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
printf("客户端(%s)已连接。\n",inet_ntoa(clientaddr.sin_addr));
// 第6步:与客户端通信,接收客户端发过来的报文后,回复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);
}
// 第7步:调用close()关闭socket
close(listenfd);
close(clientfd);
}
这里需要注意,如果第五步中同时创建了多个用于和客户端通讯的socket,那么在和每个客户端通讯结束的时候,需要注意释放socket。
客户端:
1、通过socket()函数创建用于请求和通讯的socket
2、构造主机链接地址的sockaddr_in结构体,包括sin_family,sin_port,sin_addr三个成员
3、调用connect()函数向服务端发送连接请求
4、与服务端进行通讯
5、调用close()关闭socket
// 简单的回送服务 客户端示例
#include
#include
#include
#include
#include
#include
#include
#include
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()函数创建用于请求和通讯的socket
int sockfd;
if ( (sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); return -1; }
// 第2步:构造主机链接地址的sockaddr_in结构体,包括sin_family,sin_port,sin_addr三个成员
struct hostent* h;
if ( (h = gethostbyname(argv[1])) == 0 ) // 指定服务端的ip地址。
{ printf("gethostbyname failed.\n"); close(sockfd); return -1; }
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
// 第3步:调用connect()函数向服务端发送连接请求
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) != 0) // 向服务端发起连接清求。
{ perror("connect"); close(sockfd); return -1; }
char buffer[1024];
// 第4步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
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);
}
// 第5步:调用close()关闭socket
close(sockfd);
}
#include
int socket(int domain, int type, int protocol);
// 创建成功返回文件描述符,创建失败返回-1
domain : 套接字中使用的协议族
PF_INET : IPv4互联网协议族
PF_INET6 : IPv6互联网协议族
PF_LOCAL : 本地通信的UNIX协议族
PF_PACKET : 底层套接字的协议族
PF_IPX : IPX Novell协议族
type : 套接字数据传输类型信息,套接字的数据传输方式。
SOCK_STREAM : 面向连接的套接字,TCP
可靠的,按序传递的,基于字节的面向连接的数据传输方式的套接字。
SOCK_DGRAM : UDP
不可靠的,不按序传递的,以数据的告诉传输为目的的套接字。
protocol : 计算机间通信使用的协议信息
前两个参数基本确定了协议类型,第三个参数一般传0.
IPPROTO_TCP : TCP
IPPROTO_UDP : UDP
注意:默认情况下只能打开1024(0-1023)个文件描述符,即可以创建1020个套接字,0、1、2三个描述符默认表示标准输入、输出和错误
#include
//主机字节序转网络字节序
unsigned short htons(unsigned short);
unsigned long htonl(unsigned long);
//网络字节序转主机字节序
unsigned short ntohs(unsigned short);
unsigned long ntohl(unsigned long);
// 完成字符串转换及网络字节序转换
in_addr_t inet_addr(const char *string);
// 完成字符串转换及网络字节序转换,成功返回1,失败返回0
int inet_aton(const char * string, struct in_addr *addr);
// 32位整型ip地址转换为字符串
char *inet_ntoa(struct in_addr adr);
字节顺序:字节顺序是指占内存大余一个字节类型的数据再内存中存放的顺序。(小端/大端)
由于不同的主机之中,因为CPU的不同,所以可能会使用不同的字节序去存储数据。如果客户端和服务端的字节序不同,那么解析出的数据也就不同,所以就需要规定一个统一的字节序进行数据的传输,这个统一的字节序就称为网络字节序,一般采用大端字节序。
#include /* See NOTES */
#include
//listen()将sockfd设为监听状态,并且最多允许有backlog个客户端处于连接待状态,
//如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
int listen(int sockfd, int backlog);
sockfd:socket文件描述符
backlog:在Linux 系统中,它是指已经建立好链接,等待accetp()的请求队列的长度
#include
//用于客户端
//TCP客户用connect函数来发起与TCP服务器的连接请求
//返回:若成功则为0,若出错则为-1
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
sockfd:socket函数返回的套接字描述符,
servaddr:只想套接字地址结构体的指针
addrlen:地址结构的大小。
//备注:套接字地址结构必须含有服务器的IP地址和端口号。
#include
#include
//用于服务端
//提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符,出错时返回-1
//注:客户端和服务端进行通讯所使用等socket是accept()函数返回的socket
//之前所使用等socket只能用来建立客户-服务器之间的TCP链接
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
sockfd:利用系统调用socket()建立的套接字描述符
addr:指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写。
addrlen:一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值;一般设置为sizeof(struct sockaddr_in)
如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接请求出现。
如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
一般来说,实现时accept()为阻塞函数,当监听socket调用accept()时,它先到自己的receive_buf中查看是否有连接数据包;若有,把数据拷贝出来,删掉接收到的数据包,创建新的socket与客户发来的地址建立连接,若没有,就阻塞等待;
#include
#include
//用来将数据由指定的socket 传给对方主机
//返回值:成功则返回实际传送出去的字符数, 失败返回-1. 错误原因存于errno
ssize_t send(int socket, const void * buf, int len, unsigned int falgs);
socket:为已建立好连接的socket.
buf:想要发送数据的指针
len:需要发送的数据长度,不一定和buf的大小一样
flags:一般设0, 其他数值定义如下:
MSG_OOB 传送的数据以out-of-band 送出.
MSG_DONTROUTE 取消路由表查询
MSG_DONTWAIT 设置为不可阻断运作
MSG_NOSIGNAL 此动作不愿被SIGPIPE 信号中断.
#include
#include
//recv函数用于接收对端socket发送过来的数据。
//函数返回已接收的字符数。出错时返回-1,失败时不会设置errno的值。
//如果socket的对端没有发送数据,recv函数就会阻塞
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:为已建立好连接的socket。
buf:为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。
len:需要接收数据的长度,不能超过buf的大小,否则内存溢出。
flags:填0, 其他数值意义不大。
由于TCP使用的是面向数据流的形式发送数据,所以如果短时间内同时发送多次数据,那么有可能会把这多次的数据拼接成一条数据进行发送,这个问题被称为粘包。例如,发送方发送两个字符串 “hello” 和 “world”,接收方却一次性收到了“helloworeld”。
同时相对的,如果发送的数据过长,也有可能会分为多次进行发送,这个问题称为分包,例如,发送放发送字符串“helloworld",接收方却收到两个字符串“hello” 和 “world”。
但是,TCP协议会保证:
1、接收方接收到的顺序不变
2、被分包的数据在发送的过程中,之间不会插入其他数据